From e3725634bd9463fd721feb5f22cc164b1b9f0aea Mon Sep 17 00:00:00 2001 From: Roy Wetherall Date: Wed, 21 Mar 2012 19:33:53 +0000 Subject: [PATCH 01/24] Added new SVN structure for RecordsManagement SVN archive git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/modules/recordsmangement/HEAD@34674 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 From 24a50011ae075ad246c7f656d7ede449b8b7f83d Mon Sep 17 00:00:00 2001 From: Roy Wetherall Date: Thu, 22 Mar 2012 01:36:56 +0000 Subject: [PATCH 02/24] Fix typo in folder name! git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/modules/recordsmanagement/HEAD@34676 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 From 1516161f684f4fc0ad5db842e9e1bc9e8f473a81 Mon Sep 17 00:00:00 2001 From: Roy Wetherall Date: Thu, 22 Mar 2012 02:23:49 +0000 Subject: [PATCH 03/24] Initial check-in of code into RM HEAD and first pass at Gradle build scripts. NOTE: the build scripts arn't fully working yet! git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/modules/recordsmanagement/HEAD@34677 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- build.gradle | 46 + gradle.properties | 1 + rm-server/.classpath | 17 + rm-server/.project | 17 + .../alfresco-global.properties | 19 + .../org_alfresco_module_rm/audit/rm-audit.xml | 90 + .../bootstrap/RMDataDictionaryBootstrap.xml | 177 + .../notify-records-due-for-review-email.ftl | 124 + .../bootstrap/content/onCreate_supersedes.js | 15 + .../content/record-superseded-email.ftl | 118 + .../bootstrap/content/recordsCustomModel.xml | 159 + .../content/rmEventConfigBootstrap.json | 71 + .../bootstrap/content/rma_isClosed.js | 12 + .../dod5015/DODExampleFilePlan.acp | Bin 0 -> 5214 bytes .../dod5015/DODExampleFilePlan.xml | 1046 ++ .../dod5015/dod5015-context.xml | 20 + .../dod5015/dod5015-model.properties | 82 + .../dod5015/dod5015Model.xml | 344 + .../org_alfresco_module_rm/log4j.properties | 1 + .../messages/action-service.properties | 37 + .../messages/admin-service.properties | 21 + .../messages/audit-service.properties | 34 + .../messages/notification-service.properties | 2 + .../records-management-service.properties | 17 + .../messages/records-model.properties | 273 + .../model/recordsModel.xml | 1112 +++ .../model/recordsPermissionModel.xml | 490 + .../org_alfresco_module_rm/module-context.xml | 219 + .../org_alfresco_module_rm/module.properties | 11 + .../rm-action-context.xml | 811 ++ .../rm-actions.properties | 88 + .../rm-capabilities-context.xml | 861 ++ .../rm-events.properties | 5 + .../org_alfresco_module_rm/rm-id-context.xml | 35 + .../org_alfresco_module_rm/rm-job-context.xml | 134 + .../rm-model-context.xml | 103 + .../rm-patch-context.xml | 34 + .../rm-public-services-security-context.xml | 922 ++ .../rm-service-context.xml | 1014 ++ .../rm-ui-evaluators-context.xml | 600 ++ .../rm-webscript-context.xml | 344 + .../security/rm-default-roles-bootstrap.json | 187 + .../alfresco/rma/admin/emailmap.get.desc.xml | 29 + .../alfresco/rma/admin/emailmap.get.json.ftl | 8 + .../org/alfresco/rma/admin/emailmap.lib.ftl | 14 + .../alfresco/rma/admin/emailmap.post.desc.xml | 40 + .../alfresco/rma/admin/emailmap.post.json.ftl | 7 + .../alfresco/rma/admin/emailmap.put.desc.xml | 40 + .../alfresco/rma/admin/emailmap.put.json.ftl | 8 + .../rmconstraint/rmconstraint.delete.desc.xml | 13 + .../admin/rmconstraint/rmconstraint.delete.js | 27 + .../rmconstraint/rmconstraint.delete.json.ftl | 5 + .../rmconstraint/rmconstraint.get.desc.xml | 22 + .../admin/rmconstraint/rmconstraint.get.js | 25 + .../rmconstraint/rmconstraint.get.json.ftl | 8 + .../admin/rmconstraint/rmconstraint.lib.ftl | 61 + .../rmconstraint/rmconstraint.put.desc.xml | 19 + .../rmconstraint/rmconstraint.put.json.ftl | 7 + .../rmconstraint/rmconstraint.put.json.js | 51 + .../rmconstraint/rmconstraints.get.desc.xml | 18 + .../admin/rmconstraint/rmconstraints.get.js | 19 + .../rmconstraint/rmconstraints.get.json.ftl | 13 + .../rmconstraint/rmconstraints.post.desc.xml | 20 + .../rmconstraint/rmconstraints.post.json.ftl | 7 + .../rmconstraint/rmconstraints.post.json.js | 47 + .../values/rmconstraint.get.desc.xml | 63 + .../rmconstraint/values/rmconstraint.get.js | 25 + .../values/rmconstraint.get.json.ftl | 7 + .../values/rmconstraint.post.desc.xml | 28 + .../values/rmconstraint.post.json.ftl | 7 + .../values/rmconstraint.post.json.js | 39 + .../values/rmconstraintvalue.delete.desc.xml | 11 + .../values/rmconstraintvalue.delete.js | 41 + .../values/rmconstraintvalue.delete.json.ftl | 7 + .../values/rmconstraintvalue.get.desc.xml | 47 + .../values/rmconstraintvalue.get.js | 36 + .../values/rmconstraintvalue.get.json.ftl | 7 + .../rma/admin/rmevent/rmevent.delete.desc.xml | 14 + .../rma/admin/rmevent/rmevent.delete.json.ftl | 1 + .../rma/admin/rmevent/rmevent.get.desc.xml | 14 + .../rma/admin/rmevent/rmevent.get.json.ftl | 8 + .../rma/admin/rmevent/rmevent.lib.ftl | 12 + .../rma/admin/rmevent/rmevent.put.desc.xml | 14 + .../rma/admin/rmevent/rmevent.put.json.ftl | 8 + .../rma/admin/rmevent/rmevents.get.desc.xml | 14 + .../rma/admin/rmevent/rmevents.get.json.ftl | 13 + .../rma/admin/rmevent/rmevents.post.desc.xml | 14 + .../rma/admin/rmevent/rmevents.post.json.ftl | 9 + .../admin/rmevent/rmeventtypes.get.desc.xml | 14 + .../admin/rmevent/rmeventtypes.get.json.ftl | 16 + .../rma/admin/rmrole/rmrole.delete.desc.xml | 14 + .../rma/admin/rmrole/rmrole.delete.json.ftl | 1 + .../rma/admin/rmrole/rmrole.get.desc.xml | 14 + .../rma/admin/rmrole/rmrole.get.json.ftl | 8 + .../alfresco/rma/admin/rmrole/rmrole.lib.ftl | 15 + .../rma/admin/rmrole/rmrole.put.desc.xml | 14 + .../rma/admin/rmrole/rmrole.put.json.ftl | 8 + .../rma/admin/rmrole/rmroles.get.desc.xml | 14 + .../rma/admin/rmrole/rmroles.get.json.ftl | 13 + .../rma/admin/rmrole/rmroles.post.desc.xml | 14 + .../rma/admin/rmrole/rmroles.post.json.ftl | 8 + .../rma/applydodcertmodelfixes.get.desc.xml | 15 + .../rma/applydodcertmodelfixes.get.json.ftl | 5 + .../alfresco/rma/applyfixmob1573.get.desc.xml | 9 + .../alfresco/rma/applyfixmob1573.get.json.ftl | 5 + .../rma/bootstraptestdata.get.desc.xml | 9 + .../rma/bootstraptestdata.get.json.ftl | 5 + .../alfresco/rma/customisable.get.desc.xml | 12 + .../alfresco/rma/customisable.get.json.ftl | 14 + .../rma/custompropdefinition.delete.desc.xml | 18 + .../rma/custompropdefinition.delete.json.ftl | 8 + .../rma/custompropdefinition.post.desc.xml | 29 + .../rma/custompropdefinition.post.json.ftl | 6 + .../rma/custompropdefinition.put.desc.xml | 25 + .../rma/custompropdefinition.put.json.ftl | 6 + .../rma/custompropdefinitions.get.desc.xml | 14 + .../rma/custompropdefinitions.get.json.ftl | 43 + .../alfresco/rma/customref.delete.desc.xml | 14 + .../alfresco/rma/customref.delete.json.ftl | 3 + .../org/alfresco/rma/customref.post.desc.xml | 18 + .../org/alfresco/rma/customref.post.json.ftl | 5 + .../rma/customrefdefinition.post.desc.xml | 22 + .../rma/customrefdefinition.post.json.ftl | 10 + .../rma/customrefdefinition.put.desc.xml | 25 + .../rma/customrefdefinition.put.json.ftl | 6 + .../rma/customrefdefinitions.get.desc.xml | 15 + .../rma/customrefdefinitions.get.json.ftl | 16 + .../org/alfresco/rma/customrefs.get.desc.xml | 45 + .../org/alfresco/rma/customrefs.get.json.ftl | 27 + ...ispositionactiondefinition.delete.desc.xml | 9 + ...ispositionactiondefinition.delete.json.ftl | 1 + .../rma/dispositionactiondefinition.lib.ftl | 17 + .../dispositionactiondefinition.put.desc.xml | 21 + .../dispositionactiondefinition.put.json.ftl | 5 + ...dispositionactiondefinitions.post.desc.xml | 21 + ...dispositionactiondefinitions.post.json.ftl | 5 + .../rma/dispositionlifecycle.get.desc.xml | 9 + .../rma/dispositionlifecycle.get.json.ftl | 35 + .../rma/dispositionschedule.get.desc.xml | 9 + .../rma/dispositionschedule.get.json.ftl | 29 + .../alfresco/rma/dodcustomtypes.get.desc.xml | 12 + .../alfresco/rma/dodcustomtypes.get.json.ftl | 16 + .../org/alfresco/rma/export.post.desc.xml | 16 + .../alfresco/rma/export.post.html.status.ftl | 19 + .../alfresco/rma/fileplanreport.get.desc.xml | 13 + .../org/alfresco/rma/fileplanreport.get.js | 188 + .../alfresco/rma/fileplanreport.get.json.ftl | 19 + .../org/alfresco/rma/fileplanreport.lib.ftl | 46 + .../org/alfresco/rma/import.post.desc.xml | 17 + .../org/alfresco/rma/import.post.html.ftl | 14 + .../org/alfresco/rma/import.post.json.ftl | 5 + .../alfresco/rma/listofvalues.get.desc.xml | 9 + .../alfresco/rma/listofvalues.get.json.ftl | 2 + .../org/alfresco/rma/listofvalues.lib.ftl | 75 + .../rma/recordmetadataaspects.get.desc.xml | 12 + .../rma/recordmetadataaspects.get.json.ftl | 17 + .../org/alfresco/rma/rmaction.post.desc.xml | 19 + .../org/alfresco/rma/rmaction.post.json.ftl | 14 + .../alfresco/rma/rmauditlog.delete.desc.xml | 9 + .../alfresco/rma/rmauditlog.delete.json.ftl | 2 + .../org/alfresco/rma/rmauditlog.get.desc.xml | 22 + .../org/alfresco/rma/rmauditlog.lib.ftl | 12 + .../org/alfresco/rma/rmauditlog.post.desc.xml | 23 + .../org/alfresco/rma/rmauditlog.put.desc.xml | 16 + .../org/alfresco/rma/rmauditlog.put.json.ftl | 2 + .../rma/rmauditlogstatus.get.desc.xml | 12 + .../rma/rmauditlogstatus.get.json.ftl | 6 + .../alfresco/rma/rmconstraints.get.desc.xml | 14 + .../alfresco/rma/rmconstraints.get.json.ftl | 15 + .../alfresco/rma/rmpermissions.get.desc.xml | 9 + .../org/alfresco/rma/rmpermissions.get.js | 86 + .../alfresco/rma/rmpermissions.get.json.ftl | 22 + .../alfresco/rma/rmpermissions.post.desc.xml | 36 + .../alfresco/rma/rmpermissions.post.json.ftl | 1 + .../alfresco/rma/rmpermissions.post.json.js | 52 + .../org/alfresco/rma/transfer.get.desc.xml | 9 + .../alfresco/rma/transferreport.get.desc.xml | 9 + .../alfresco/rma/transferreport.post.desc.xml | 16 + .../rma/userrightsreport.get.desc.xml | 9 + .../rma/userrightsreport.get.json.ftl | 46 + .../rm-doclist.get.desc.xml | 12 + .../documentlibrary-v2/rm-doclist.get.js | 10 + .../rm-doclist.get.json.ftl | 1 + .../documentlibrary-v2/rm-filters.lib.js | 197 + .../documentlibrary/rm-doclist.lib.js | 178 + .../documentlibrary/rm-evaluator.lib.js | 547 + .../documentlibrary/rm-filters.lib.js | 183 + .../documentlibrary/rm-node.get.desc.xml | 9 + .../slingshot/documentlibrary/rm-node.get.js | 6 + .../documentlibrary/rm-node.get.json.ftl | 34 + .../rm-savedsearches.get.desc.xml | 9 + .../documentlibrary/rm-savedsearches.get.js | 54 + .../rm-savedsearches.get.json.ftl | 13 + .../documentlibrary/rm-transfer.get.desc.xml | 9 + .../documentlibrary/rm-transfer.get.js | 21 + .../documentlibrary/rm-transfer.get.json.ftl | 13 + .../documentlibrary/rm-treenode.get.desc.xml | 12 + .../documentlibrary/rm-treenode.get.js | 130 + .../documentlibrary/rm-treenode.get.json.ftl | 39 + .../rmsearch/rmsavedsearches.delete.desc.xml | 9 + .../rmsearch/rmsavedsearches.delete.json.ftl | 3 + .../rmsearch/rmsavedsearches.get.desc.xml | 9 + .../rmsearch/rmsavedsearches.get.json.ftl | 16 + .../rmsearch/rmsavedsearches.post.desc.xml | 9 + .../rmsearch/rmsavedsearches.post.json.ftl | 3 + .../slingshot/rmsearch/rmsearch.get.desc.xml | 9 + .../slingshot/rmsearch/rmsearch.get.json.ftl | 42 + rm-server/gradle.properties | 2 + .../FilePlanComponentKind.java | 39 + .../RecordsManagementAdminService.java | 388 + .../RecordsManagementAdminServiceImpl.java | 1540 +++ .../RecordsManagementBootstrap.java | 105 + .../RecordsManagementPolicies.java | 79 + .../RecordsManagementPoliciesUtil.java | 71 + .../RecordsManagementService.java | 462 + .../RecordsManagementServiceImpl.java | 1179 +++ .../RecordsManagementServiceRegistry.java | 88 + .../RecordsManagementServiceRegistryImpl.java | 92 + .../action/RMActionExecuterAbstractBase.java | 609 ++ ...DispositionActionExecuterAbstractBase.java | 393 + .../action/RecordsManagementAction.java | 106 + .../action/RecordsManagementActionResult.java | 49 + .../RecordsManagementActionService.java | 115 + .../RecordsManagementActionServiceImpl.java | 293 + .../action/ScheduledDispositionJob.java | 129 + .../action/impl/ApplyCustomTypeAction.java | 159 + ...spositionActionDefinitionUpdateAction.java | 293 + .../action/impl/CloseRecordFolderAction.java | 104 + .../action/impl/CompleteEventAction.java | 189 + .../impl/CreateDispositionScheduleAction.java | 73 + .../action/impl/CutOffAction.java | 139 + .../action/impl/DeclareRecordAction.java | 236 + .../action/impl/DestroyAction.java | 233 + .../EditDispositionActionAsOfDateAction.java | 122 + .../action/impl/EditHoldReasonAction.java | 119 + .../action/impl/EditReviewAsOfDateAction.java | 114 + .../action/impl/FileAction.java | 169 + .../action/impl/FreezeAction.java | 225 + .../action/impl/OpenRecordFolderAction.java | 104 + .../action/impl/RelinquishHoldAction.java | 200 + .../action/impl/RetainAction.java | 43 + .../action/impl/SetupRecordFolderAction.java | 83 + .../action/impl/SplitEmailAction.java | 326 + .../action/impl/TransferAction.java | 215 + .../action/impl/TransferCompleteAction.java | 144 + .../action/impl/UnCutoffAction.java | 128 + .../action/impl/UndeclareRecordAction.java | 96 + .../action/impl/UndoEventAction.java | 219 + .../action/impl/UnfreezeAction.java | 159 + .../audit/AuditEvent.java | 68 + .../AuthenticatedUserRolesDataExtractor.java | 111 + .../FilePlanIdentifierDataExtractor.java | 70 + .../audit/FilePlanNamePathDataExtractor.java | 92 + .../FilePlanNodeRefPathDataExtractor.java | 84 + .../audit/RecordsManagementAuditEntry.java | 268 + ...RecordsManagementAuditQueryParameters.java | 205 + .../audit/RecordsManagementAuditService.java | 169 + .../RecordsManagementAuditServiceImpl.java | 1326 +++ .../capability/Capability.java | 88 + .../capability/CapabilityService.java | 67 + .../capability/CapabilityServiceImpl.java | 104 + .../capability/RMActionProxyFactoryBean.java | 69 + .../capability/RMAfterInvocationProvider.java | 977 ++ .../capability/RMEntryVoter.java | 1091 ++ .../capability/RMPermissionModel.java | 175 + .../capability/RMSecurityCommon.java | 215 + .../AbstractCapabilityCondition.java | 85 + .../declarative/CapabilityCondition.java | 28 + .../declarative/DeclarativeCapability.java | 289 + .../condition/ClosedCapabilityCondition.java | 60 + .../condition/CutoffCapabilityCondition.java | 38 + .../DeclaredCapabilityCondition.java | 34 + .../DestroyedCapabilityCondition.java | 36 + .../FileableCapabilityCondition.java | 57 + .../condition/FillingCapabilityCondition.java | 117 + .../condition/FrozenCapabilityCondition.java | 47 + .../condition/FrozenOrHoldCondition.java | 43 + .../HasEventsCapabilityCondition.java | 63 + .../IsScheduledCapabilityCondition.java | 77 + .../MayBeScheduledCapabilityCondition.java | 105 + .../TransferredCapabilityCondition.java | 35 + ...italRecordOrFolderCapabilityCondition.java | 52 + .../capability/group/CreateCapability.java | 160 + .../capability/group/DeclareCapability.java | 54 + .../capability/group/DeleteCapability.java | 67 + .../capability/group/UpdateCapability.java | 95 + .../group/UpdatePropertiesCapability.java | 97 + .../group/WriteContentCapability.java | 55 + .../capability/impl/AbstractCapability.java | 278 + .../ChangeOrDeleteReferencesCapability.java | 81 + .../impl/DeleteLinksCapability.java | 64 + .../impl/EditRecordMetadataCapability.java | 99 + .../impl/FileRecordsCapability.java | 115 + .../impl/MoveRecordsCapability.java | 92 + .../impl/ViewRecordsCapability.java | 43 + .../caveat/PivotUtil.java | 55 + .../caveat/RMCaveatConfigComponent.java | 112 + .../caveat/RMCaveatConfigComponentImpl.java | 945 ++ .../caveat/RMCaveatConfigService.java | 153 + .../caveat/RMCaveatConfigServiceImpl.java | 427 + .../caveat/RMConstraintInfo.java | 63 + .../caveat/RMListOfValuesConstraint.java | 229 + .../caveat/ScriptAuthority.java | 51 + .../caveat/ScriptConstraint.java | 264 + .../caveat/ScriptConstraintAuthority.java | 49 + .../caveat/ScriptConstraintValue.java | 57 + .../caveat/ScriptRMCaveatConfigService.java | 166 + .../disposition/DispositionAction.java | 94 + .../DispositionActionDefinition.java | 112 + .../DispositionActionDefinitionImpl.java | 219 + .../disposition/DispositionActionImpl.java | 220 + .../DispositionPeriodProperties.java | 51 + .../disposition/DispositionSchedule.java | 82 + .../disposition/DispositionScheduleImpl.java | 185 + .../DispositionSelectionStrategy.java | 170 + .../disposition/DispositionService.java | 188 + .../disposition/DispositionServiceImpl.java | 626 ++ .../dod5015/DOD5015Model.java | 78 + .../email/CustomEmailMappingService.java | 32 + .../email/CustomEmailMappingServiceImpl.java | 411 + .../email/CustomMapping.java | 96 + .../email/RFC822MetadataExtracter.java | 111 + .../event/EventCompletionDetails.java | 162 + .../event/OnReferenceCreateEventType.java | 175 + .../event/OnReferencedRecordActionedUpon.java | 256 + .../event/RecordsManagementEvent.java | 79 + .../event/RecordsManagementEventService.java | 92 + .../RecordsManagementEventServiceImpl.java | 280 + .../event/RecordsManagementEventType.java | 48 + .../SimpleRecordsManagementEventTypeImpl.java | 98 + .../forms/RecordsManagementFormFilter.java | 142 + .../RecordsManagementNodeFormFilter.java | 438 + .../RecordsManagementTypeFormFilter.java | 219 + .../identifier/BasicIdentifierGenerator.java | 57 + .../identifier/IdentifierGenerator.java | 45 + .../identifier/IdentifierGeneratorBase.java | 103 + .../identifier/IdentifierService.java | 59 + .../identifier/IdentifierServiceImpl.java | 196 + .../job/DispositionLifecycleJob.java | 150 + .../job/NotifyOfRecordsDueForReviewJob.java | 143 + .../job/PublishUpdatesJob.java | 320 + .../job/publish/BasePublishExecutor.java | 47 + ...sitionActionDefinitionPublishExecutor.java | 102 + .../job/publish/PublishExecutor.java | 41 + .../job/publish/PublishExecutorRegistry.java | 54 + .../jscript/ScriptCapability.java | 65 + .../jscript/ScriptRecordsManagmentNode.java | 90 + .../ScriptRecordsManagmentService.java | 122 + .../jscript/app/BaseEvaluator.java | 233 + .../jscript/app/JSONConversionComponent.java | 233 + .../app/evaluator/CutoffEvaluator.java | 43 + .../evaluator/FolderOpenClosedEvaluator.java | 41 + .../app/evaluator/FrozenEvaluator.java | 34 + .../app/evaluator/HasAspectEvaluator.java | 45 + .../app/evaluator/MultiParentEvaluator.java | 40 + .../app/evaluator/NonElectronicEvaluator.java | 49 + .../evaluator/SplitEmailActionEvaluator.java | 54 + .../app/evaluator/TransferEvaluator.java | 55 + .../jscript/app/evaluator/TrueEvaluator.java | 36 + .../app/evaluator/VitalRecordEvaluator.java | 42 + .../model/CustomisableTypesBootstrap.java | 78 + .../model/FilePlanComponentAspect.java | 205 + .../RecordComponentIdentifierAspect.java | 172 + .../model/RecordContainerType.java | 191 + .../model/RecordCopyBehaviours.java | 253 + .../model/RecordsManagementCustomModel.java | 52 + .../model/RecordsManagementModel.java | 204 + .../RecordsManagementSearchBehaviour.java | 699 ++ .../model/RmSiteType.java | 139 + .../model/ScheduledAspect.java | 92 + .../RecordsManagementNotificationHelper.java | 293 + .../patch/NotificationTemplatePatch.java | 187 + .../patch/RMv2ModelPatch.java | 161 + .../script/AbstractRmWebScript.java | 147 + .../script/ApplyDodCertModelFixesGet.java | 210 + .../script/ApplyFixMob1573Get.java | 146 + .../script/AuditLogDelete.java | 49 + .../script/AuditLogGet.java | 88 + .../script/AuditLogPost.java | 139 + .../script/AuditLogPut.java | 87 + .../script/AuditLogStatusGet.java | 60 + .../script/BaseAuditAdminWebScript.java | 62 + .../script/BaseAuditRetrievalWebScript.java | 249 + .../script/BaseCustomPropertyWebScript.java | 64 + .../script/BaseTransferWebScript.java | 202 + .../script/BootstrapTestDataGet.java | 325 + .../CustomPropertyDefinitionDelete.java | 113 + .../script/CustomPropertyDefinitionPost.java | 235 + .../script/CustomPropertyDefinitionPut.java | 178 + .../script/CustomPropertyDefinitionsGet.java | 118 + .../script/CustomRefDelete.java | 112 + .../script/CustomRefPost.java | 115 + .../script/CustomReferenceDefinitionPost.java | 174 + .../script/CustomReferenceDefinitionPut.java | 153 + .../script/CustomReferenceDefinitionsGet.java | 159 + .../script/CustomReferenceType.java | 55 + .../script/CustomRefsGet.java | 179 + .../script/CustomisableGet.java | 174 + .../script/DispositionAbstractBase.java | 155 + .../DispositionActionDefinitionDelete.java | 55 + .../DispositionActionDefinitionPost.java | 147 + .../DispositionActionDefinitionPut.java | 145 + .../script/DispositionLifecycleGet.java | 183 + .../script/DispositionScheduleGet.java | 98 + .../script/DodCustomTypesGet.java | 69 + .../script/EmailMapGet.java | 67 + .../script/EmailMapPost.java | 110 + .../script/EmailMapPut.java | 108 + .../script/ExportPost.java | 155 + .../script/ImportPost.java | 252 + .../script/ListOfValuesGet.java | 304 + .../script/RMConstraintGet.java | 71 + .../script/RecordMetaDataAspectsGet.java | 107 + .../script/RmActionPost.java | 224 + .../script/TransferGet.java | 86 + .../script/TransferReportGet.java | 287 + .../script/TransferReportPost.java | 428 + .../script/UserRightsReportGet.java | 354 + .../script/admin/RmEventDelete.java | 83 + .../script/admin/RmEventGet.java | 82 + .../script/admin/RmEventPut.java | 122 + .../script/admin/RmEventTypesGet.java | 68 + .../script/admin/RmEventsGet.java | 68 + .../script/admin/RmEventsPost.java | 121 + .../script/admin/RmRoleDelete.java | 85 + .../script/admin/RmRoleGet.java | 87 + .../script/admin/RmRolePut.java | 123 + .../script/admin/RmRolesGet.java | 85 + .../script/admin/RmRolesPost.java | 111 + .../slingshot/RMSavedSearchesDelete.java | 95 + .../script/slingshot/RMSavedSearchesGet.java | 157 + .../script/slingshot/RMSavedSearchesPost.java | 169 + .../script/slingshot/RMSearchGet.java | 445 + .../RecordsManagementSearchParameters.java | 370 + .../RecordsManagementSearchService.java | 92 + .../RecordsManagementSearchServiceImpl.java | 612 ++ .../search/ReportDetails.java | 111 + .../search/SavedSearchDetails.java | 287 + .../SavedSearchDetailsCompatibility.java | 229 + .../RecordsManagementSecurityService.java | 186 + .../RecordsManagementSecurityServiceImpl.java | 975 ++ .../org_alfresco_module_rm/security/Role.java | 79 + .../test/CapabilitiesTestSuite.java | 47 + .../test/DOD5015Test.java | 4582 +++++++++ .../test/JScriptTestSuite.java | 45 + .../test/ServicesTestSuite.java | 57 + .../test/WebScriptTestSuite.java | 57 + .../AddModifyEventDatesCapabilityTest.java | 282 + ...veRecordsScheduledForCutoffCapability.java | 307 + .../capabilities/BaseCapabilitiesTest.java | 922 ++ .../capabilities/BaseTestCapabilities.java | 903 ++ .../test/capabilities/CapabilitiesTest.java | 3841 +++++++ .../DeclarativeCapabilityTest.java | 245 + .../test/jscript/CapabilitiesTest.js | 18 + .../jscript/JSONConversionComponentTest.java | 159 + .../test/jscript/RMJScriptTest.java | 78 + .../service/DispositionServiceImplTest.java | 800 ++ .../RMCaveatConfigServiceImplTest.java | 612 ++ ...ecordsManagementActionServiceImplTest.java | 290 + ...RecordsManagementAdminServiceImplTest.java | 952 ++ ...RecordsManagementAuditServiceImplTest.java | 427 + ...RecordsManagementEventServiceImplTest.java | 139 + ...ecordsManagementSearchServiceImplTest.java | 311 + ...ordsManagementSecurityServiceImplTest.java | 692 ++ .../RecordsManagementServiceImplTest.java | 670 ++ .../service/VitalRecordServiceImplTest.java | 381 + .../test/system/CapabilitiesSystemTest.java | 8849 +++++++++++++++++ .../test/system/DODDataLoadSystemTest.java | 78 + .../NotificationServiceHelperSystemTest.java | 158 + .../system/PerformanceDataLoadSystemTest.java | 253 + ...ecordsManagementServiceImplSystemTest.java | 812 ++ .../test/util/BaseRMTestCase.java | 635 ++ .../test/util/TestAction.java | 57 + .../test/util/TestAction2.java | 51 + .../test/util/TestActionParams.java | 52 + .../test/util/TestUtilities.java | 238 + .../test/util/TestWebScriptRepoServer.java | 221 + .../test/util/test-context.xml | 70 + .../test/util/test-model.xml | 45 + .../BootstraptestDataRestApiTest.java | 78 + .../webscript/DispositionRestApiTest.java | 633 ++ .../test/webscript/EmailMapScriptTest.java | 150 + .../test/webscript/EventRestApiTest.java | 257 + .../webscript/RMCaveatConfigScriptTest.java | 971 ++ .../webscript/RMConstraintScriptTest.java | 164 + .../test/webscript/RmRestApiTest.java | 1604 +++ .../test/webscript/RoleRestApiTest.java | 309 + .../BroadcastVitalRecordDefinitionAction.java | 136 + .../vital/ReviewedAction.java | 121 + .../vital/VitalRecordDefinition.java | 56 + .../vital/VitalRecordDefinitionImpl.java | 94 + .../vital/VitalRecordService.java | 58 + .../vital/VitalRecordServiceImpl.java | 229 + .../test-resources/testCaveatConfig1.json | 8 + .../test-resources/testCaveatConfig2.json | 27 + settings.gradle | 1 + 496 files changed, 88729 insertions(+) create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 rm-server/.classpath create mode 100644 rm-server/.project create mode 100644 rm-server/config/alfresco/module/org_alfresco_module_rm/alfresco-global.properties create mode 100644 rm-server/config/alfresco/module/org_alfresco_module_rm/audit/rm-audit.xml create mode 100644 rm-server/config/alfresco/module/org_alfresco_module_rm/bootstrap/RMDataDictionaryBootstrap.xml create mode 100644 rm-server/config/alfresco/module/org_alfresco_module_rm/bootstrap/content/notify-records-due-for-review-email.ftl create mode 100644 rm-server/config/alfresco/module/org_alfresco_module_rm/bootstrap/content/onCreate_supersedes.js create mode 100644 rm-server/config/alfresco/module/org_alfresco_module_rm/bootstrap/content/record-superseded-email.ftl create mode 100644 rm-server/config/alfresco/module/org_alfresco_module_rm/bootstrap/content/recordsCustomModel.xml create mode 100644 rm-server/config/alfresco/module/org_alfresco_module_rm/bootstrap/content/rmEventConfigBootstrap.json create mode 100644 rm-server/config/alfresco/module/org_alfresco_module_rm/bootstrap/content/rma_isClosed.js create mode 100644 rm-server/config/alfresco/module/org_alfresco_module_rm/dod5015/DODExampleFilePlan.acp create mode 100644 rm-server/config/alfresco/module/org_alfresco_module_rm/dod5015/DODExampleFilePlan.xml create mode 100644 rm-server/config/alfresco/module/org_alfresco_module_rm/dod5015/dod5015-context.xml create mode 100644 rm-server/config/alfresco/module/org_alfresco_module_rm/dod5015/dod5015-model.properties create mode 100644 rm-server/config/alfresco/module/org_alfresco_module_rm/dod5015/dod5015Model.xml create mode 100644 rm-server/config/alfresco/module/org_alfresco_module_rm/log4j.properties create mode 100644 rm-server/config/alfresco/module/org_alfresco_module_rm/messages/action-service.properties create mode 100644 rm-server/config/alfresco/module/org_alfresco_module_rm/messages/admin-service.properties create mode 100644 rm-server/config/alfresco/module/org_alfresco_module_rm/messages/audit-service.properties create mode 100644 rm-server/config/alfresco/module/org_alfresco_module_rm/messages/notification-service.properties create mode 100644 rm-server/config/alfresco/module/org_alfresco_module_rm/messages/records-management-service.properties create mode 100644 rm-server/config/alfresco/module/org_alfresco_module_rm/messages/records-model.properties create mode 100644 rm-server/config/alfresco/module/org_alfresco_module_rm/model/recordsModel.xml create mode 100644 rm-server/config/alfresco/module/org_alfresco_module_rm/model/recordsPermissionModel.xml create mode 100644 rm-server/config/alfresco/module/org_alfresco_module_rm/module-context.xml create mode 100644 rm-server/config/alfresco/module/org_alfresco_module_rm/module.properties create mode 100644 rm-server/config/alfresco/module/org_alfresco_module_rm/rm-action-context.xml create mode 100644 rm-server/config/alfresco/module/org_alfresco_module_rm/rm-actions.properties create mode 100644 rm-server/config/alfresco/module/org_alfresco_module_rm/rm-capabilities-context.xml create mode 100644 rm-server/config/alfresco/module/org_alfresco_module_rm/rm-events.properties create mode 100644 rm-server/config/alfresco/module/org_alfresco_module_rm/rm-id-context.xml create mode 100644 rm-server/config/alfresco/module/org_alfresco_module_rm/rm-job-context.xml create mode 100644 rm-server/config/alfresco/module/org_alfresco_module_rm/rm-model-context.xml create mode 100644 rm-server/config/alfresco/module/org_alfresco_module_rm/rm-patch-context.xml create mode 100644 rm-server/config/alfresco/module/org_alfresco_module_rm/rm-public-services-security-context.xml create mode 100644 rm-server/config/alfresco/module/org_alfresco_module_rm/rm-service-context.xml create mode 100644 rm-server/config/alfresco/module/org_alfresco_module_rm/rm-ui-evaluators-context.xml create mode 100644 rm-server/config/alfresco/module/org_alfresco_module_rm/rm-webscript-context.xml create mode 100644 rm-server/config/alfresco/module/org_alfresco_module_rm/security/rm-default-roles-bootstrap.json create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/emailmap.get.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/emailmap.get.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/emailmap.lib.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/emailmap.post.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/emailmap.post.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/emailmap.put.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/emailmap.put.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraint.delete.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraint.delete.js create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraint.delete.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraint.get.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraint.get.js create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraint.get.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraint.lib.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraint.put.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraint.put.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraint.put.json.js create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraints.get.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraints.get.js create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraints.get.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraints.post.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraints.post.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraints.post.json.js create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraint.get.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraint.get.js create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraint.get.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraint.post.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraint.post.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraint.post.json.js create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraintvalue.delete.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraintvalue.delete.js create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraintvalue.delete.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraintvalue.get.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraintvalue.get.js create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraintvalue.get.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevent.delete.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevent.delete.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevent.get.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevent.get.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevent.lib.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevent.put.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevent.put.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevents.get.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevents.get.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevents.post.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevents.post.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmeventtypes.get.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmeventtypes.get.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmrole.delete.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmrole.delete.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmrole.get.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmrole.get.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmrole.lib.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmrole.put.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmrole.put.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmroles.get.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmroles.get.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmroles.post.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmroles.post.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/applydodcertmodelfixes.get.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/applydodcertmodelfixes.get.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/applyfixmob1573.get.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/applyfixmob1573.get.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/bootstraptestdata.get.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/bootstraptestdata.get.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customisable.get.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customisable.get.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/custompropdefinition.delete.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/custompropdefinition.delete.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/custompropdefinition.post.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/custompropdefinition.post.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/custompropdefinition.put.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/custompropdefinition.put.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/custompropdefinitions.get.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/custompropdefinitions.get.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customref.delete.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customref.delete.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customref.post.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customref.post.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customrefdefinition.post.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customrefdefinition.post.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customrefdefinition.put.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customrefdefinition.put.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customrefdefinitions.get.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customrefdefinitions.get.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customrefs.get.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customrefs.get.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionactiondefinition.delete.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionactiondefinition.delete.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionactiondefinition.lib.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionactiondefinition.put.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionactiondefinition.put.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionactiondefinitions.post.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionactiondefinitions.post.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionlifecycle.get.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionlifecycle.get.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionschedule.get.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionschedule.get.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dodcustomtypes.get.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dodcustomtypes.get.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/export.post.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/export.post.html.status.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/fileplanreport.get.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/fileplanreport.get.js create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/fileplanreport.get.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/fileplanreport.lib.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/import.post.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/import.post.html.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/import.post.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/listofvalues.get.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/listofvalues.get.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/listofvalues.lib.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/recordmetadataaspects.get.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/recordmetadataaspects.get.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmaction.post.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmaction.post.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmauditlog.delete.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmauditlog.delete.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmauditlog.get.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmauditlog.lib.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmauditlog.post.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmauditlog.put.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmauditlog.put.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmauditlogstatus.get.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmauditlogstatus.get.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmconstraints.get.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmconstraints.get.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmpermissions.get.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmpermissions.get.js create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmpermissions.get.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmpermissions.post.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmpermissions.post.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmpermissions.post.json.js create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/transfer.get.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/transferreport.get.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/transferreport.post.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/userrightsreport.get.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/userrightsreport.get.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary-v2/rm-doclist.get.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary-v2/rm-doclist.get.js create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary-v2/rm-doclist.get.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary-v2/rm-filters.lib.js create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-doclist.lib.js create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-evaluator.lib.js create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-filters.lib.js create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-node.get.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-node.get.js create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-node.get.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-savedsearches.get.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-savedsearches.get.js create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-savedsearches.get.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-transfer.get.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-transfer.get.js create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-transfer.get.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-treenode.get.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-treenode.get.js create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-treenode.get.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/rmsearch/rmsavedsearches.delete.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/rmsearch/rmsavedsearches.delete.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/rmsearch/rmsavedsearches.get.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/rmsearch/rmsavedsearches.get.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/rmsearch/rmsavedsearches.post.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/rmsearch/rmsavedsearches.post.json.ftl create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/rmsearch/rmsearch.get.desc.xml create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/rmsearch/rmsearch.get.json.ftl create mode 100644 rm-server/gradle.properties create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/FilePlanComponentKind.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/RecordsManagementAdminService.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/RecordsManagementAdminServiceImpl.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/RecordsManagementBootstrap.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/RecordsManagementPolicies.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/RecordsManagementPoliciesUtil.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/RecordsManagementService.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/RecordsManagementServiceImpl.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/RecordsManagementServiceRegistry.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/RecordsManagementServiceRegistryImpl.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/RMActionExecuterAbstractBase.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/RMDispositionActionExecuterAbstractBase.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/RecordsManagementAction.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/RecordsManagementActionResult.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/RecordsManagementActionService.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/RecordsManagementActionServiceImpl.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/ScheduledDispositionJob.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/ApplyCustomTypeAction.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/BroadcastDispositionActionDefinitionUpdateAction.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/CloseRecordFolderAction.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/CompleteEventAction.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/CreateDispositionScheduleAction.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/CutOffAction.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/DeclareRecordAction.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/DestroyAction.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/EditDispositionActionAsOfDateAction.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/EditHoldReasonAction.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/EditReviewAsOfDateAction.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/FileAction.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/FreezeAction.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/OpenRecordFolderAction.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/RelinquishHoldAction.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/RetainAction.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/SetupRecordFolderAction.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/SplitEmailAction.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/TransferAction.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/TransferCompleteAction.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/UnCutoffAction.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/UndeclareRecordAction.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/UndoEventAction.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/UnfreezeAction.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/audit/AuditEvent.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/audit/AuthenticatedUserRolesDataExtractor.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/audit/FilePlanIdentifierDataExtractor.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/audit/FilePlanNamePathDataExtractor.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/audit/FilePlanNodeRefPathDataExtractor.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/audit/RecordsManagementAuditEntry.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/audit/RecordsManagementAuditQueryParameters.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/audit/RecordsManagementAuditService.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/audit/RecordsManagementAuditServiceImpl.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/Capability.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/CapabilityService.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/CapabilityServiceImpl.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/RMActionProxyFactoryBean.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/RMAfterInvocationProvider.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/RMEntryVoter.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/RMPermissionModel.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/RMSecurityCommon.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/AbstractCapabilityCondition.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/CapabilityCondition.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/DeclarativeCapability.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/ClosedCapabilityCondition.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/CutoffCapabilityCondition.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/DeclaredCapabilityCondition.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/DestroyedCapabilityCondition.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/FileableCapabilityCondition.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/FillingCapabilityCondition.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/FrozenCapabilityCondition.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/FrozenOrHoldCondition.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/HasEventsCapabilityCondition.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/IsScheduledCapabilityCondition.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/MayBeScheduledCapabilityCondition.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/TransferredCapabilityCondition.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/VitalRecordOrFolderCapabilityCondition.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/CreateCapability.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/DeclareCapability.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/DeleteCapability.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/UpdateCapability.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/UpdatePropertiesCapability.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/WriteContentCapability.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/AbstractCapability.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/ChangeOrDeleteReferencesCapability.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/DeleteLinksCapability.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/EditRecordMetadataCapability.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/FileRecordsCapability.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/MoveRecordsCapability.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/ViewRecordsCapability.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/PivotUtil.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/RMCaveatConfigComponent.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/RMCaveatConfigComponentImpl.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/RMCaveatConfigService.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/RMCaveatConfigServiceImpl.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/RMConstraintInfo.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/RMListOfValuesConstraint.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/ScriptAuthority.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/ScriptConstraint.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/ScriptConstraintAuthority.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/ScriptConstraintValue.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/ScriptRMCaveatConfigService.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionAction.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionActionDefinition.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionActionDefinitionImpl.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionActionImpl.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionPeriodProperties.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionSchedule.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionScheduleImpl.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionSelectionStrategy.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionService.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionServiceImpl.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/dod5015/DOD5015Model.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/email/CustomEmailMappingService.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/email/CustomEmailMappingServiceImpl.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/email/CustomMapping.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/email/RFC822MetadataExtracter.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/event/EventCompletionDetails.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/event/OnReferenceCreateEventType.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/event/OnReferencedRecordActionedUpon.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/event/RecordsManagementEvent.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/event/RecordsManagementEventService.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/event/RecordsManagementEventServiceImpl.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/event/RecordsManagementEventType.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/event/SimpleRecordsManagementEventTypeImpl.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/forms/RecordsManagementFormFilter.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/forms/RecordsManagementNodeFormFilter.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/forms/RecordsManagementTypeFormFilter.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/identifier/BasicIdentifierGenerator.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/identifier/IdentifierGenerator.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/identifier/IdentifierGeneratorBase.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/identifier/IdentifierService.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/identifier/IdentifierServiceImpl.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/job/DispositionLifecycleJob.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/job/NotifyOfRecordsDueForReviewJob.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/job/PublishUpdatesJob.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/job/publish/BasePublishExecutor.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/job/publish/DispositionActionDefinitionPublishExecutor.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/job/publish/PublishExecutor.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/job/publish/PublishExecutorRegistry.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/ScriptCapability.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/ScriptRecordsManagmentNode.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/ScriptRecordsManagmentService.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/BaseEvaluator.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/JSONConversionComponent.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/evaluator/CutoffEvaluator.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/evaluator/FolderOpenClosedEvaluator.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/evaluator/FrozenEvaluator.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/evaluator/HasAspectEvaluator.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/evaluator/MultiParentEvaluator.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/evaluator/NonElectronicEvaluator.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/evaluator/SplitEmailActionEvaluator.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/evaluator/TransferEvaluator.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/evaluator/TrueEvaluator.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/evaluator/VitalRecordEvaluator.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/CustomisableTypesBootstrap.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/FilePlanComponentAspect.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordComponentIdentifierAspect.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordContainerType.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordCopyBehaviours.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordsManagementCustomModel.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordsManagementModel.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordsManagementSearchBehaviour.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/RmSiteType.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/ScheduledAspect.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/notification/RecordsManagementNotificationHelper.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/patch/NotificationTemplatePatch.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/patch/RMv2ModelPatch.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/AbstractRmWebScript.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/ApplyDodCertModelFixesGet.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/ApplyFixMob1573Get.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/AuditLogDelete.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/AuditLogGet.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/AuditLogPost.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/AuditLogPut.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/AuditLogStatusGet.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/BaseAuditAdminWebScript.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/BaseAuditRetrievalWebScript.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/BaseCustomPropertyWebScript.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/BaseTransferWebScript.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/BootstrapTestDataGet.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomPropertyDefinitionDelete.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomPropertyDefinitionPost.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomPropertyDefinitionPut.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomPropertyDefinitionsGet.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomRefDelete.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomRefPost.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomReferenceDefinitionPost.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomReferenceDefinitionPut.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomReferenceDefinitionsGet.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomReferenceType.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomRefsGet.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomisableGet.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/DispositionAbstractBase.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/DispositionActionDefinitionDelete.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/DispositionActionDefinitionPost.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/DispositionActionDefinitionPut.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/DispositionLifecycleGet.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/DispositionScheduleGet.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/DodCustomTypesGet.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/EmailMapGet.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/EmailMapPost.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/EmailMapPut.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/ExportPost.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/ImportPost.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/ListOfValuesGet.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/RMConstraintGet.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/RecordMetaDataAspectsGet.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/RmActionPost.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/TransferGet.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/TransferReportGet.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/TransferReportPost.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/UserRightsReportGet.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmEventDelete.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmEventGet.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmEventPut.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmEventTypesGet.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmEventsGet.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmEventsPost.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmRoleDelete.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmRoleGet.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmRolePut.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmRolesGet.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmRolesPost.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/slingshot/RMSavedSearchesDelete.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/slingshot/RMSavedSearchesGet.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/slingshot/RMSavedSearchesPost.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/slingshot/RMSearchGet.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/search/RecordsManagementSearchParameters.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/search/RecordsManagementSearchService.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/search/RecordsManagementSearchServiceImpl.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/search/ReportDetails.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/search/SavedSearchDetails.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/search/SavedSearchDetailsCompatibility.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/security/RecordsManagementSecurityService.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/security/RecordsManagementSecurityServiceImpl.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/security/Role.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/CapabilitiesTestSuite.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/DOD5015Test.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/JScriptTestSuite.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/ServicesTestSuite.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/WebScriptTestSuite.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/AddModifyEventDatesCapabilityTest.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/ApproveRecordsScheduledForCutoffCapability.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/BaseCapabilitiesTest.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/BaseTestCapabilities.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/CapabilitiesTest.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/DeclarativeCapabilityTest.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/jscript/CapabilitiesTest.js create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/jscript/JSONConversionComponentTest.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/jscript/RMJScriptTest.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/DispositionServiceImplTest.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RMCaveatConfigServiceImplTest.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementActionServiceImplTest.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementAdminServiceImplTest.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementAuditServiceImplTest.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementEventServiceImplTest.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementSearchServiceImplTest.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementSecurityServiceImplTest.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementServiceImplTest.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/VitalRecordServiceImplTest.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/system/CapabilitiesSystemTest.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/system/DODDataLoadSystemTest.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/system/NotificationServiceHelperSystemTest.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/system/PerformanceDataLoadSystemTest.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/system/RecordsManagementServiceImplSystemTest.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/BaseRMTestCase.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestAction.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestAction2.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestActionParams.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestUtilities.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestWebScriptRepoServer.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/test-context.xml create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/test-model.xml create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/BootstraptestDataRestApiTest.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/DispositionRestApiTest.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/EmailMapScriptTest.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/EventRestApiTest.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RMCaveatConfigScriptTest.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RMConstraintScriptTest.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RmRestApiTest.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RoleRestApiTest.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/vital/BroadcastVitalRecordDefinitionAction.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/vital/ReviewedAction.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/vital/VitalRecordDefinition.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/vital/VitalRecordDefinitionImpl.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/vital/VitalRecordService.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/vital/VitalRecordServiceImpl.java create mode 100644 rm-server/test-resources/testCaveatConfig1.json create mode 100644 rm-server/test-resources/testCaveatConfig2.json create mode 100644 settings.gradle diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000000..ec61a39005 --- /dev/null +++ b/build.gradle @@ -0,0 +1,46 @@ +subprojects { + + apply plugin: 'java' + + sourceSets { + main { + java { + srcDir 'source/java' + } + } + } + + repositories { + flatDir { + dirs 'libs' + } + mavenCentral() + } + + dependencies { + + compile fileTree(dir: 'libs', include: '*.jar') + + compile fileTree(dir: 'libs/test', include: '*.jar') + compile group: 'javax.servlet', name: 'servlet-api', version: '2.5' + compile 'org.springframework:spring-test:2.5' + } + + task unpackWar << { + + println 'Unpacking ' + warFileName + ' WAR ...' + + // Clean out any existing jars + ant.delete { + ant.fileset(dir: 'libs', includes: '*.jar') + } + + // Unpack WAR + ant.unzip(src: warFile, dest: 'libs') { + ant.patternset { + ant.include(name: '**/*.jar') + } + ant.mapper(type: 'flatten') + } + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ + diff --git a/rm-server/.classpath b/rm-server/.classpath new file mode 100644 index 0000000000..e191c98be8 --- /dev/null +++ b/rm-server/.classpath @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/rm-server/.project b/rm-server/.project new file mode 100644 index 0000000000..ec8bf86f5d --- /dev/null +++ b/rm-server/.project @@ -0,0 +1,17 @@ + + + Records Management + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/alfresco-global.properties b/rm-server/config/alfresco/module/org_alfresco_module_rm/alfresco-global.properties new file mode 100644 index 0000000000..ffbe1a1259 --- /dev/null +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/alfresco-global.properties @@ -0,0 +1,19 @@ +# Enable ghosting of records on deletion +rm.ghosting.enabled=true + +# Notification configuration +rm.notification.role=RecordsManager +# NOTE: the notification subject can now be set within the usual I18N property files per notification template + +# +# Turn off imap server attachments if we are using RM. +# TODO : Longer term needs to have a query based, dynamic +# exclusion for RM sites. +# +imap.server.attachments.extraction.enabled=false + +# +# Enable auditing +# +audit.enabled=true +audit.rm.enabled=true \ No newline at end of file diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/audit/rm-audit.xml b/rm-server/config/alfresco/module/org_alfresco_module_rm/audit/rm-audit.xml new file mode 100644 index 0000000000..6ceab3c551 --- /dev/null +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/audit/rm-audit.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/bootstrap/RMDataDictionaryBootstrap.xml b/rm-server/config/alfresco/module/org_alfresco_module_rm/bootstrap/RMDataDictionaryBootstrap.xml new file mode 100644 index 0000000000..57ee9dac21 --- /dev/null +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/bootstrap/RMDataDictionaryBootstrap.xml @@ -0,0 +1,177 @@ + + + + + + + workspace + SpacesStore + rm_config_folder + Records Management + Records Management + Configuration information for the Records Management application. + + + + + + + + + + + workspace + SpacesStore + rm_event_config + Records management event configuration. + contentUrl=classpath:alfresco/module/org_alfresco_module_rm/bootstrap/content/rmEventConfigBootstrap.json|mimetype=text/plain|encoding=UTF-8 + rm_event_config.json + rm_event_config.json + + + + + + + + + workspace + SpacesStore + records_management_custom_model + Records Management Custom Model + contentUrl=classpath:alfresco/module/org_alfresco_module_rm/bootstrap/content/recordsCustomModel.xml|mimetype=text/plain|encoding=UTF-8 + recordsCustomModel.xml + recordsCustomModel.xml + {http://www.alfresco.org/model/rmcustom/1.0}rmc + Records Management Custom Model + Alfresco + 1.0 + true + + + + + + + workspace + SpacesStore + rm_scripts + Records Management Scripts + Records Management Scripts + Scripts intended for execution in response to RM events. + + + + + + + + + + workspace + SpacesStore + Records management sample script. + contentUrl=classpath:alfresco/module/org_alfresco_module_rm/bootstrap/content/rma_isClosed.js|mimetype=text/javascript|encoding=UTF-8 + rma_isClosed.js + rma_isClosed.js + + + + + + + + + workspace + SpacesStore + Records management sample script. + contentUrl=classpath:alfresco/module/org_alfresco_module_rm/bootstrap/content/onCreate_supersedes.js|mimetype=text/javascript|encoding=UTF-8 + onCreate_supersedes.js + onCreate_supersedes.js + + + + + + + + + + workspace + SpacesStore + records_management_email_templates + Records Management Email Templates + Records Management Email Templates + Email templates for records management. + + + + + + + + + + + + + + true + + Email template for notify records due for review job. + contentUrl=classpath:alfresco/module/org_alfresco_module_rm/bootstrap/content/notify-records-due-for-review-email.ftl|mimetype=text/plain|size=|encoding=UTF-8|locale=en_US_ + notify-records-due-for-review-email.ftl + + notify-records-due-for-review-email.ftl + org_alfresco_module_rm_notificationTemplatePatch + + + + + + + + + + + + workspace + SpacesStore + record_superseded_template + Record superseded email template. + contentUrl=classpath:alfresco/module/org_alfresco_module_rm/bootstrap/content/record-superseded-email.ftl|mimetype=text/plain|encoding=UTF-8 + record-superseded-email.ftl + record-superseded-email.ftl + org_alfresco_module_rm_notificationTemplatePatch + + + + + + + + + + + \ No newline at end of file diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/bootstrap/content/notify-records-due-for-review-email.ftl b/rm-server/config/alfresco/module/org_alfresco_module_rm/bootstrap/content/notify-records-due-for-review-email.ftl new file mode 100644 index 0000000000..acf23fce72 --- /dev/null +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/bootstrap/content/notify-records-due-for-review-email.ftl @@ -0,0 +1,124 @@ + + + + + + + + + + +
+ + + + +
+ + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + +
+ + +
+ Records due for review. +
+
+ ${date?datetime?string.full} +
+
+
+

Hi,

+ +

The following records are now due for review:

+ + <#if (args.records)??> + + <#list args.records as record> + + + + <#if record_has_next> + + + +
+ + + + + +
+ + + + + + + + + + + + +
${record.properties["rma:identifier"]!} ${record.name}
Click on this link to view the record:
+ + ${shareUrl}/page/site/${args.site}/document-details?nodeRef=${record.storeType}://${record.storeId}/${record.id} +
+
+
+ + +

Sincerely,
+ Alfresco ${productName!""}

+
+
+
+
 
+
+ To find out more about Alfresco ${productName!""} visit http://www.alfresco.com +
+
 
+
+ +
+
+
+ + \ No newline at end of file diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/bootstrap/content/onCreate_supersedes.js b/rm-server/config/alfresco/module/org_alfresco_module_rm/bootstrap/content/onCreate_supersedes.js new file mode 100644 index 0000000000..330a794f50 --- /dev/null +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/bootstrap/content/onCreate_supersedes.js @@ -0,0 +1,15 @@ +/** + * Main entrypoint for script. + * + * @method main + */ +function main() +{ + // Log debug message + logger.log("Record " + node.name + " has been superseded. Sending notification"); + + // Send notification + rmService.sendSupersededNotification(node); +} + +main(); diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/bootstrap/content/record-superseded-email.ftl b/rm-server/config/alfresco/module/org_alfresco_module_rm/bootstrap/content/record-superseded-email.ftl new file mode 100644 index 0000000000..e15b1024f3 --- /dev/null +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/bootstrap/content/record-superseded-email.ftl @@ -0,0 +1,118 @@ + + + + + + + + + + +
+ + + + +
+ + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + +
+ + +
+ Superseded record. +
+
+ ${date?datetime?string.full} +
+
+
+

Hi,

+ +

The following record been superseded:

+ + + + + + +
+ + + + + +
+ + + + + + + + + + + + +
${args.record.properties["rma:identifier"]!} ${args.record.name}
Click on this link to view the record:
+ + ${shareUrl}/page/site/${args.site}/document-details?nodeRef=${args.record.storeType}://${args.record.storeId}/${args.record.id} +
+
+
+ +

Sincerely,
+ Alfresco ${productName!""}

+
+
+
+
 
+
+ To find out more about Alfresco ${productName!""} visit http://www.alfresco.com +
+
 
+
+ +
+
+
+ + diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/bootstrap/content/recordsCustomModel.xml b/rm-server/config/alfresco/module/org_alfresco_module_rm/bootstrap/content/recordsCustomModel.xml new file mode 100644 index 0000000000..46f9d6b45c --- /dev/null +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/bootstrap/content/recordsCustomModel.xml @@ -0,0 +1,159 @@ + + + + + + + + + Records Management Custom Model + Alfresco + 1.0 + + + + + + + + + + + + + + + + + + + + + + + Supplemental Markings + + + + + true + + + + + Transfer Locations + + + + + true + + + + + + + + + + Supplemental Marking List + d:text + false + true + + + + + + + + + + Records Management Custom Associations + + + + + SupersededBy__Supersedes + + false + true + + + rma:record + false + true + + + + + ObsoletedBy__Obsoletes + + false + true + + + rma:record + false + true + + + + + VersionedBy__Versions + + false + true + + + rma:record + false + true + + + + + Supporting Documentation__Supported Documentation + + false + true + + + rma:record + false + true + + + + + Cross-Reference + + false + true + + + rma:record + false + true + + + + + Rendition + + false + true + + + rma:record + false + true + + + + + + + + + \ No newline at end of file diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/bootstrap/content/rmEventConfigBootstrap.json b/rm-server/config/alfresco/module/org_alfresco_module_rm/bootstrap/content/rmEventConfigBootstrap.json new file mode 100644 index 0000000000..07cab10ffa --- /dev/null +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/bootstrap/content/rmEventConfigBootstrap.json @@ -0,0 +1,71 @@ +{ + "events" : + [ + { + "eventType" : "rmEventType.simple", + "eventName" : "case_closed", + "eventDisplayLabel" : "Case Closed" + }, + { + "eventType" : "rmEventType.simple", + "eventName" : "abolished", + "eventDisplayLabel" : "Abolished" + }, + { + "eventType" : "rmEventType.simple", + "eventName" : "re_designated", + "eventDisplayLabel" : "Redesignated" + }, + { + "eventType" : "rmEventType.simple", + "eventName" : "no_longer_needed", + "eventDisplayLabel" : "No longer needed" + }, + { + "eventType" : "rmEventType.superseded", + "eventName" : "superseded", + "eventDisplayLabel" : "Superseded" + }, + { + "eventType" : "rmEventType.simple", + "eventName" : "study_complete", + "eventDisplayLabel" : "Study Complete" + }, + { + "eventType" : "rmEventType.simple", + "eventName" : "training_complete", + "eventDisplayLabel" : "Training Complete" + }, + { + "eventType" : "rmEventType.crossReferencedRecordTransfered", + "eventName" : "related_record_trasfered_inactive_storage", + "eventDisplayLabel" : "Related Record Transfered To Inactive Storage" + }, + { + "eventType" : "rmEventType.obsolete", + "eventName" : "obsolete", + "eventDisplayLabel" : "Obsolete" + }, + { + "eventType" : "rmEventType.simple", + "eventName" : "all_allowances_granted_are_terminated", + "eventDisplayLabel" : "All Allowances Granted Are Terminated" + }, + { + "eventType" : "rmEventType.simple", + "eventName" : "WGI_action_complete", + "eventDisplayLabel" : "WGI action complete" + }, + { + "eventType" : "rmEventType.simple", + "eventName" : "separation", + "eventDisplayLabel" : "Separation" + }, + { + "eventType" : "rmEventType.simple", + "eventName" : "case_complete", + "eventDisplayLabel" : "Case Complete" + } + ] +} + diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/bootstrap/content/rma_isClosed.js b/rm-server/config/alfresco/module/org_alfresco_module_rm/bootstrap/content/rma_isClosed.js new file mode 100644 index 0000000000..1728d5c903 --- /dev/null +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/bootstrap/content/rma_isClosed.js @@ -0,0 +1,12 @@ +/** + * Main entrypoint for script. + * This sample script simply echoes the name of the node with the changed property. + * + * @method main + */ +function main() +{ + logger.log("Sample RM script. No-op run on node " + node.name); +} + +main(); diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/dod5015/DODExampleFilePlan.acp b/rm-server/config/alfresco/module/org_alfresco_module_rm/dod5015/DODExampleFilePlan.acp new file mode 100644 index 0000000000000000000000000000000000000000..a7d23b0890f256471ef6c84bf70b735694cf4c11 GIT binary patch literal 5214 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 YO9ci1000010096*000046aWAK0RNE>4gdfE literal 0 HcmV?d00001 diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/dod5015/DODExampleFilePlan.xml b/rm-server/config/alfresco/module/org_alfresco_module_rm/dod5015/DODExampleFilePlan.xml new file mode 100644 index 0000000000..9b40dfe679 --- /dev/null +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/dod5015/DODExampleFilePlan.xml @@ -0,0 +1,1046 @@ + + + + + + + Reports + 0318 + Reports + Record series for reports + + + + + + + + + + + + + AIS Audit Records + 0318-01 + AIS Audit Records + Consisting of AIS Security Officer or Terminal Area Security Officer weekly audit records of audit actions performed on all AIS as required by applicable policy which are maintained by any JS/combatant command activity. + week|1 + true + + + + + + + + + N1-218-00-4 item 023 + Cut off monthly, hold 1 month, then destroy. + + + + + + + + cutoff + monthend|1 + + + + + destroy + month|1 + {http://www.alfresco.org/model/recordsmanagement/1.0}cutOffDate + + + + + + + + + + + + + + January AIS Audit Records + January AIS Audit Records + 0318-01-01 + week|1 + true + + + + + + + + + + + + + + + Unit Manning Documents + 0318-02 + Unit Manning Documents + Consisting of manpower document and monthly strength report forwarded to OSD and other activities which are maintained by personnel office as the official record copy. + + + + + + + + + N1-218-89-1 item 002 + Cut off every 3 months, hold 3 months, then destroy. + + + + + + + + cutoff + quarterend|1 + + + + + destroy + month|3 + {http://www.alfresco.org/model/recordsmanagement/1.0}cutOffDate + + + + + + + + + + + + + 1st Quarter Unit Manning Documents + 0318-02-01 + 1st Quarter Unit Manning Documents + + + + + + + + + + + + + + + Overtime Reports + 0318-03 + Overtime reports and related documents + Overtime reports and related documents which are maintained by JS/combatant controller as the official record copy. + + + + + + + + + N1-218-00-7 item 28 + Cut off at end of FY, hold 3 years, then destroy. + + + + + + + cutoff + fyend|1 + + + + + destroy + year|3 + {http://www.alfresco.org/model/recordsmanagement/1.0}cutOffDate + + + + + + + + + + + + FY08 Overtime Reports + 0318-03-01 + FY08 Overtime Reports + + + + + + + + + + + + + + + Bi-Weekly Cost Reports + 0318-04 + Bi-Weekly Cost Reports + Bi-wekly cost reports which are maintained by JS/combatant command controler as the official record copy. + + + + + + + + + N1-218-00-7 item 2 + Cut off at end of CY, hold 2 years, then destroy. + + + + + + + cutoff + yearend|1 + + + + + destroy + year|2 + {http://www.alfresco.org/model/recordsmanagement/1.0}cutOffDate + + + + + + + + + + + CY08 Unit Manning Documents + 0318-04-01 + CY08 Unit Manning Documents + + + + + + + + + + + + + + + + Military Files + 0412 + Military Files + Record series for military files + + + + + + + + + + + + + Military Assignment Documents + 0412-01 + Military Assignment Documents + Policy matters pertaining to military assignments which are maintained by any JS/combatant command activity as the official record copy. + + + + + + + + + N1-218-00-3 item 30 + Cut off when superseded, hold 5 years, then destroy. + true + + + + + + + cutoff + superseded + + + + + destroy + year|5 + {http://www.alfresco.org/model/recordsmanagement/1.0}cutOffDate + + + + + + + + + + + + + + + + + + + + + Official Military Personnel Privilege Card Applications + 0412-02 + Official Military Personnel Privilege Card Applications + Consisting of: documents reflecting applications for priviege cards and ration cards, including Department of Defense Forms (DD Forms) 1172 (Application for Unifomed Services Identification and Privilege Card) and similar documents which are maintained by any JS/combatant command activity. + + + + + + + + + N1-218-00-3 item 20 + Cut off when no longer needed and destroy immediately. + + + + + + + cutoff + no_longer_needed + + + + + destroy + immediately|0 + + + + + + + + + + + + COL Bob Johnson + 0412-02-01 + COL Bob Johnson + + + + + PFC Alan Murphy + 0412-02-02 + PFC Alan Murphy + + + + + + + + + + + + + + + Personnel Security Program Records + 0412-03 + Personnel Security Program Records + Position sensitivity files including requests for information relating to the designation of sensitive and non-sensitive personnel positions in an agency and results of final actions taken consisting of approved requests which are maintained by any JS/combatant command activity + + + + + + + + + N1-218-00-4 item 017 + Cutoff when position is abolished, re-designated, or no longer needed, whichever is later. Destroy immediately after cutoff. + + + + + + + cutoff + + abolished + re_designated + no_longer_needed + + and + + + + + destroy + immediately|0 + + + + + + + + + + + + Commander's Administrative Assistant + 0412-03-01 + Commander's Administrative Assistant + + + + + Equal Opportunity Coordinator + 0412-03-02 + Equal Opportunity Coordinator + + + + + + + + + + + + + + + + Civilian Files + 0430 + Civilian Files + Record series for civilian files + + + + + + + + + + + + + Employee Performance File System Records + 0430-01 + Employee Performance File System Records + Consisting of: performance records superseded through an administrative, judicial, or quasi-judicial procedure which are maintained by any JS/combatant command activity + + + + + + + + + GRS 1 item 23b(1) + Cutoff when superseded. Destroy immediately after cutoff + true + + + + + + + cutoff + superseded + + + + + destroy + immediately|0 + + + + + + + + + + + + + + + + + + + Foreign Employee Award Files + 0430-02 + Foreign Employee Award Files + Decorations to foreign nationals and US citizens not employed by the US Government consisting of: case files of recommendations, decisions, awards announcements, board meeting minutes, and related documents which are maintained by any JS/combatant command activity + + + + + + + + N1-218-00-3 item 18 + Permanent. Cut off on completion of case, hold 2 years, then retire to offline storage. Transfer to federal records holding area 5 years after retirement to offline storage. Transfer to NARA 25 years after cutoff. + + + + + + + cutoff + case_complete + + + + + transfer + Retire to offline storage. + Offline Storage + year|2 + {http://www.alfresco.org/model/recordsmanagement/1.0}cutOffDate + + + + + transfer + Transfer to federal records holding area. + Federal Records Holding + year|5 + {http://www.alfresco.org/model/recordsmanagement/1.0}dispositionAsOf + + + + + accession + Transfer to NARA. + NARA + year|25 + {http://www.alfresco.org/model/recordsmanagement/1.0}cutOffDate + + + + + + + + + + + + Christian Bohr + 0430-02-01 + Christian Bohr + + + + + Karl Planck + 0430-02-02 + Karl Planck + + + + + + + + + + + + + + + Case Files and Papers + 0430-03 + Case Files and Papers + Consisting of library containing information on personnel actions which are maintained by R&A Br and Deputy Chief Information Office + None + Disposal not authorized. Disposition pending NARA approval. + + + + + + + Gilbert Competency Hearing + 0430-03-01 + Gilbert Competency Hearing + + + + + + + + + + + + + + + Withholding of Within-Grade Increase (WGI) Records + 0430-04 + Withholding of Within-Grade Increase (WGI) Records + Files concerning an employee’s performance rating of record with work examples which establish less than fully successful performance, notice of withholding of WGI, employee's request for reconsideration of denied WGI, and decision concerning such a reconsideration request which are maintained by any JS/combatant command activity. + + + + + + + + + N1-218-00-3 item 16 + Cut off on completion of WGI action or on separation, whichever is earlier; hold 3 years, then destroy/delete. + + + + + + + cutoff + + WGI_action_complete + separation + + or + + + + + destroy + year|3 + {http://www.alfresco.org/model/recordsmanagement/1.0}cutOffDate + + + + + + + + + + + + Gilbert WGI Records + 0430-04-01 + Gilbert WGI Records + + + + + + + + + + + + + + + Payroll Differential and Allowances + 0430-05 + Payroll Differential and Allowances + Consisting of: information to assist overseas civilian personnel offices to document employee eligibility for foreign post differential and foreign quarters and post allowances, including SF 1190 (Foreign Allowances Application, Grant, and Report) and similar information which are maintained by any JS/combatant command activity. + + + + + + + + + N1-218-00-3 item 3 + Cut off at end of Fiscal Year (FY) in which all allowances granted are terminated, hold 3 years, then destroy. + + + + + + + retain + all_allowances_granted_are_terminated + + + + + cutoff + fyend|1 + {http://www.alfresco.org/model/recordsmanagement/1.0}dispositionAsOf + + + + + destroy + year|3 + {http://www.alfresco.org/model/recordsmanagement/1.0}cutOffDate + + + + + + + + + + + + Martin Payroll Differential and Allowances + 0430-05-01 + Martin Payroll Differential and Allowances + + + + + + + + + + + + + + + + Miscellaneous Files + 0950 + Miscellaneous Files + Record series for miscellaneous files + + + + + + + + + + + + + Civilian Employee Training Program Records + 0950-01 + Civilian Employee Training Program Records + Decorations to foreign nationals and US citizens not employed by the US Government consisting of: case files of recommendations, decisions, awards announcements, board meeting minutes, and related documents which are maintained by any JS/combatant command activity + + + + + + + + GRS 1 item 29b + Cut off annually, hold 5 years, then destroy, or destroy when obsolete, whichever is earlier. + + + + + + + cutoff + year|1 + {http://www.alfresco.org/model/recordsmanagement/1.0}dateFiled + + + + + destroy + year|5 + {http://www.alfresco.org/model/recordsmanagement/1.0}cutOffDate + obsolete + + + + + + + + + + + + Bob Prentice Training Records (2008) + 0950-01-01 + Bob Prentice Training Records (2008) + + + + + Beth Tanaka Training Records (2008) + 0950-01-02 + Beth Tanaka Training Records (2008) + + + + + Chuck Stevens Training Records (2008) + 0950-01-03 + Chuck Stevens Training Records (2008) + + + + + + + + + + + + + + + Purchase of Foreign Award Medals and Decorations + 0950-02 + Purchase of Foreign Award Medals and Decorations + Forms reflecting purchase of foreign award medals and decorations. + + + + + + + + + N1-218-00-3 item 11 + Cutoff when related record is transferred to inactive storage, hold 1 year, destroy. + true + + + + + + + cutoff + related_record_trasfered_inactive_storage + + + + + destroy + year|1 + {http://www.alfresco.org/model/recordsmanagement/1.0}cutOffDate + + + + + + + + + + + + + + + + + + + + Monthly Cockpit Crew Training + 0950-03 + Monthly Cockpit/Crew Training + Consisting of skills training/evaluation forms, e.g., AF Form 4031. + + + + + + + + + N1-218-00-3 item 13 + Cutoff after training is complete, hold 1 year, destroy. + + + + + + + cutoff + training_complete + + + + + destroy + year|1 + {http://www.alfresco.org/model/recordsmanagement/1.0}cutOffDate + + + + + + + + + + + + January Cockpit Crew Training + 0950-03-01 + January Cockpit/Crew Training + + + + + February Cockpit Crew Training + 0950-03-02 + February Cockpit/Crew Training + + + + + + + + + + + + + + + Science Advisor Records + 0950-04 + Science Advisor Records + Consisting of: reports, studies, tasking orders, and similar records. Reports are usually informal and unpublished. Records may be generated at all activities + + + + + + + + + N1-218-00-10 item 44 + Cut off on completion of study, hold 5 years, then transfer to Inactive Storage. Transfer to NARA 25 years after cutoff. + + + + + + + cutoff + study_complete + + + + + transfer + Transfer to inactive storage. + Inactive Storage + year|5 + {http://www.alfresco.org/model/recordsmanagement/1.0}cutOffDate + + + + + accession + Transfer to NARA. + NARA + year|25 + {http://www.alfresco.org/model/recordsmanagement/1.0}cutOffDate + + + + + + + + + + + + Phoenix Mars Mission + 0950-04-01 + Phoenix Mars Mission + + + + + + + + + + + + + \ No newline at end of file diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/dod5015/dod5015-context.xml b/rm-server/config/alfresco/module/org_alfresco_module_rm/dod5015/dod5015-context.xml new file mode 100644 index 0000000000..bb2d6a5f11 --- /dev/null +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/dod5015/dod5015-context.xml @@ -0,0 +1,20 @@ + + + + + + + + + + alfresco/module/org_alfresco_module_rm/dod5015/dod5015Model.xml + + + + + alfresco/module/org_alfresco_module_rm/dod5015/dod5015-model + + + + + \ No newline at end of file diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/dod5015/dod5015-model.properties b/rm-server/config/alfresco/module/org_alfresco_module_rm/dod5015/dod5015-model.properties new file mode 100644 index 0000000000..235924f786 --- /dev/null +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/dod5015/dod5015-model.properties @@ -0,0 +1,82 @@ +dod_dod5015.description=DOD5015 Content Model + +dod_dod5015.type.dod_filePlan.title=File Plan +dod_dod5015.type.dod_filePlan.description=File Plan + +dod_dod5015.type.dod_recordSeries.title=Record Series +dod_dod5015.type.dod_recordSeries.description=Record Series + +dod_dod5015.type.dod_recordCategory.title=Record Category +dod_dod5015.type.dod_recordCategory.description=Record Category + +dod_dod5015.aspect.dod_scannedRecord.title=Scanned Record +dod_dod5015.aspect.dod_scannedRecord.description=Scanned Record +dod_dod5015.property.dod_scannedFormat.title=Image Format +dod_dod5015.property.dod_scannedFormat.description=Image Format +dod_dod5015.property.dod_scannedFormatVersion.title=Image Format and Version +dod_dod5015.property.dod_scannedFormatVersion.description=Image Format and Version +dod_dod5015.property.dod_resolutionX.title=Image Resolution X +dod_dod5015.property.dod_resolutionX.description=Image Resolution X +dod_dod5015.property.dod_resolutionY.title=Image Resolution Y +dod_dod5015.property.dod_resolutionY.description=Image Resolution Y +dod_dod5015.property.dod_scannedBitDepth.title=Scanned Bit Depth +dod_dod5015.property.dod_scannedBitDepth.description=Scanned Bit Depth + +dod_dod5015.aspect.dod_pdfRecord.title=PDF Record +dod_dod5015.aspect.dod_pdfRecord.description=PDF Record +dod_dod5015.property.dod_producingApplication.title=Producing Application +dod_dod5015.property.dod_producingApplication.description=Producing Application +dod_dod5015.property.dod_producingApplicationVersion.title=Producing Application Version +dod_dod5015.property.dod_producingApplicationVersion.description=Producing Application Version +dod_dod5015.property.dod_pdfVersion.title=PDF Version +dod_dod5015.property.dod_pdfVersion.description=PDF Version +dod_dod5015.property.dod_creatingApplication.title=Creating Application +dod_dod5015.property.dod_creatingApplication.description=Creating Application +dod_dod5015.property.dod_documentSecuritySettings.title=Document Security Settings +dod_dod5015.property.dod_documentSecuritySettings.description=Document Security Settings + +dod_dod5015.aspect.dod_digitalPhotographRecord.title=Digital Photograph Record +dod_dod5015.aspect.dod_digitalPhotographRecord.description=Digital Photograph Record +dod_dod5015.property.dod_caption.title=Caption +dod_dod5015.property.dod_caption.description=Caption +dod_dod5015.property.dod_photographer.title=Photographer +dod_dod5015.property.dod_photographer.description=Photographer +dod_dod5015.property.dod_copyright.title=Copyright +dod_dod5015.property.dod_copyright.description=Copyright +dod_dod5015.property.dod_bitDepth.title=Bit Depth +dod_dod5015.property.dod_bitDepth.description=Bit Depth +dod_dod5015.property.dod_imageSizeX.title=Image Size X +dod_dod5015.property.dod_imageSizeX.description=Image Size X +dod_dod5015.property.dod_imageSizeY.title=Image Size Y +dod_dod5015.property.dod_imageSizeY.description=Image Size Y +dod_dod5015.property.dod_imageSource.title=Image Source +dod_dod5015.property.dod_imageSource.description=Image Source +dod_dod5015.property.dod_compression.title=Compression +dod_dod5015.property.dod_compression.description=Compression +dod_dod5015.property.dod_iccIcmProfile.title=ICC/ICM Profile +dod_dod5015.property.dod_iccIcmProfile.description=ICC/ICM Profile +dod_dod5015.property.dod_exifInformation.title=EXIF Information +dod_dod5015.property.dod_exifInformation.description=EXIF Information + +dod_dod5015.aspect.dod_webRecord.title=Web Record +dod_dod5015.aspect.dod_webRecord.description=Web Record +dod_dod5015.property.dod_webFileName.title=Web File Name +dod_dod5015.property.dod_webFileName.description=Web File Name +dod_dod5015.property.dod_webPlatform.title=Web Platform +dod_dod5015.property.dod_webPlatform.description=Web Platform +dod_dod5015.property.dod_webSiteName.title=Web Site Name +dod_dod5015.property.dod_webSiteName.description=Web Site Name +dod_dod5015.property.dod_webSiteURL.title=Web Site URL +dod_dod5015.property.dod_webSiteURL.description=Web Site URL +dod_dod5015.property.dod_captureMethod.title=Capture Method +dod_dod5015.property.dod_captureMethod.description=Capture Method +dod_dod5015.property.dod_captureDate.title=Capture Date +dod_dod5015.property.dod_captureDate.description=Capture Date +dod_dod5015.property.dod_contact.title=Contact +dod_dod5015.property.dod_contact.description=Contact +dod_dod5015.property.dod_contentManagementSystem.title=Content Management System +dod_dod5015.property.dod_contentManagementSystem.description=Content Management System + +dod_dod5015.aspect.dod_ghosted.title=Ghosted Record +dod_dod5015.aspect.dod_ghosted.description=Ghosted Record + diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/dod5015/dod5015Model.xml b/rm-server/config/alfresco/module/org_alfresco_module_rm/dod5015/dod5015Model.xml new file mode 100644 index 0000000000..c96a58b304 --- /dev/null +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/dod5015/dod5015Model.xml @@ -0,0 +1,344 @@ + + + + + + + + DOD 5015 Model + Roy Wetherall + 1.0 + + + + + + + + + + + + + + + + + + + + + Image Formats + + + Binary Image Interchange Format (BIIF) + GIF 89a + Graphic Image Format (GIF) 87a + Joint Photographic Experts Group (JPEG) (all versions) + Portable Network Graphics (PNG) 1.0 + Tagged Image Interchange Format (TIFF) 4.0 + TIFF 5.0 + TIFF 6.0 + + + true + + + + + + + + Record Series + rma:recordCategory + + + + + + + + Scanned Record + rma:recordMetaData + + + Image Format + d:text + + true + false + false + + + + Image Format and Version + d:text + true + + true + false + false + + + + + + + Image Resolution X + d:int + true + + + Image Resolution Y + d:int + true + + + Scanned Bit Depth + d:int + false + + + + rma:filePlanComponent + + + + + PDF Record + rma:recordMetaData + + + Producing Application + d:text + true + + true + false + false + + + + Producing Application Version + d:text + true + + + PDF Version + d:text + true + + true + false + false + + + + Creating Application + d:text + false + + true + false + false + + + + Document Security Settings + d:text + false + + true + false + false + + + + + rma:filePlanComponent + + + + + Digital Photograph Record + rma:recordMetaData + + + Caption + d:text + true + + + Photographer + d:text + false + + true + false + false + + + + Copyright + d:text + false + + true + false + false + + + + Bit Depth + d:text + false + + true + false + false + + + + Image Size X + d:int + false + + + Image Size Y + d:int + false + + + Image Source + d:text + false + + true + false + false + + + + Compression + d:text + false + + true + false + false + + + + ICC/ICM Profile + d:text + false + + true + false + false + + + + EXIF Information + d:text + false + + true + false + false + + + + + rma:filePlanComponent + + + + + Web Record + rma:recordMetaData + + + Web File Name + d:text + true + + true + false + false + + + + Web Platform + d:text + true + + true + false + false + + + + Web Site Name + d:text + true + + true + false + false + + + + Web Site URL + d:text + true + + true + false + false + + + + Capture Method + d:text + true + + true + false + false + + + + Capture Date + d:date + true + + + Contact + d:text + true + + true + false + false + + + + Content Management System + d:text + false + + true + false + false + + + + + rma:filePlanComponent + + + + + + \ No newline at end of file diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/log4j.properties b/rm-server/config/alfresco/module/org_alfresco_module_rm/log4j.properties new file mode 100644 index 0000000000..bdf0de5022 --- /dev/null +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/log4j.properties @@ -0,0 +1 @@ +log4j.logger.org.alfresco.module.org_alfresco_module_rm.caveat=warn diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/messages/action-service.properties b/rm-server/config/alfresco/module/org_alfresco_module_rm/messages/action-service.properties new file mode 100644 index 0000000000..c55856199e --- /dev/null +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/messages/action-service.properties @@ -0,0 +1,37 @@ +rm.action.not-defined=The records management action {0} has not been defined. +rm.action.no-implicit-noderef=Unable to execute the records management action, because the action {0} implementation does not provide an implicit nodeRef. +rm.action.record-not-declared=Unable to execute disposition action {0}, because the record is not declared. (actionedUponNodeRef={1}) +rm.action.expected-record-level=Unable to execute disposition action {0}, because disposition is expected at the record level and this node is not a record. (actionedUponNodeRef={1}) +rm.action.not-all-records-declared=Unable to execute disposition action {0}, because not all the records in the record are declared. (actionedUponNodeRef={1}) +rm.action.not-eligible=Unable to execute disposition action {0}, because the next disposition action on the record or record folder is not eligible. (actionedUponNodeRef={1}) +rm.action.no-disposition-instructions=Unable to find disposition instructions for node. Can not execute disposition action {0}. (nodeRef={1}) +rm.action.no-disposition-lisfecycle-set=Unable to execute disposition action {0}, because node does not have a disposition life-cycle set. (nodeRef={1}) +rm.action.next-disp-not-set=Unable to execute disposition action {0}, because the next disposition action is not set. (nodeRef={1}) +rm.action.not-next-disp=Unable to execute disposition action {0}, because this is not the next disposition action for this record or record folder. (nodeRef={1}) +rm.action.not-record-folder=Unable to execute disposition action {0}, because disposition is expected at the record folder level and this node is not a record folder. (nodeRef={1}) +rm.action.actioned-upon-not-record=Can not execute action {0}, because the actioned upon node is not a Record. (filePlanComponet={1}) +rm.action.custom-aspect-not-recognised=The custom type can not be applied, because is it not recognised. (customAspect={0}) +rm.action.close-record-folder-not-folder=Unable to close record folder, because the node is not a record folder. (nodeRef={0}) +rm.action.event-no-disp-lc=The event {0} can not be completed, because it is not defined on the disposition lifecycle. +rm.action.undeclared-only-records=Only records can be undeclared. (nodeRef={0}) +rm.action.no-declare-mand-prop=Can not declare record, because not all the records mandatory properties have been set. +rm.action.ghosted-prop-update=The content properties of a previously destroyed record can not be updated. +rm.action.valid-date-disp-asof=A valid date must be specified when setting the disposition action as of date. +rm.action.disp-asof-lifecycle-applied=It is invalid to edit the disposition as of date of a record or record folder which has a lifecycle applied. +rm.action.hold-edit-reason-none=Can not edit hold reason, because no reason has been given. +rm.action.hold-edit-type=Can not edit hold reason, because actioned upon node is not of type {0}. (nodeRef={1}) +rm.action.specify-avlid-date=Must specify a valid date when setting the review as of date. +rm.action.review-details-only=Can only edit the review details of vital records. +rm.action.freeze-no-reason=Can not freeze a record without a reason. +rm.action.freeze-only-records-folders=Can only freeze records or record folders. +rm.action.no-open-record-folder=Unable to open record folder, because node is not a record folder. (actionedUponNodeRef={0}) +rm.action.not-hold-type=Can not relinquish hold, because node is not of type {0}. (actionedUponNodeRef={1}) +rm.action.no-read-mime-message=Unable to read mime message, because {0}. +rm.action.email-declared=Can not split email, because record has already been declared. (actionedUponNodeRef={0}) +rm.action.email-not-record=Can no split email, because node is not a record. (actionedUponNodeRef={0}) +rm.action.email-create-child-assoc=Unable to create custom child association. +rm.action.node-already-transfer=Node is already being transfered. +rm.action.node-not-transfer=Node is not a transfer object. +rm.action.undo-not-last=Can not undo cut off, because last disposition action was not cut off. +rm.action.records_only_undeclared=Only records can be undeclared. +rm.action.event-not-undone=The event {0} can not be undone, because it is not defined on the disposition lifecycle. \ No newline at end of file diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/messages/admin-service.properties b/rm-server/config/alfresco/module/org_alfresco_module_rm/messages/admin-service.properties new file mode 100644 index 0000000000..09d209b7dd --- /dev/null +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/messages/admin-service.properties @@ -0,0 +1,21 @@ +rm.admin.service-not-init=The customisation service has not been initialised. +rm.admin.not-customisable=The class {0} is not customisable. +rm.admin.invalid-custom-aspect=Unable to find custom aspect {0} for customisable class {1}. +rm.admin.property-already-exists=Property {0} already exists. +rm.admin.cannot-apply-constraint=Cannot apply constraint {0} to property {1} with datatype {2}. (expected: dataType = TEXT) +rm.admin.prop-exist=Can not find custom property {0}. +rm.admin.custom-prop-exist=Custom model does not contain property {0}. +rm.admin.unknown-aspect=Unknown aspect {0}. +rm.admin.ref-exist= Can not find custom reference {0}. +rm.admin.ref-label-in-use=Reference label {0} is already in use. +rm.admin.assoc-exists=Association {0} already exists. +rm.admin.child-assoc-exists=Child association {0} already exists. +rm.admin.cannot-find-assoc-def=Can not find association definition {0}. +rm.admin.constraint-exists=Constraint {0} already exists. +rm.admin.contraint-cannot-find=Can not find definition for constraint {0}. +rm.admin.unexpected_type_constraint=Unexpected type {0} for constraint {1}, expected {2}. +rm.admin.custom-model-not-found=Custom model {0} can not be found. +rm.admin.custom-model-no-content=Custom model has no content. (nodeRef={0}) +rm.admin.error-write-custom-model=Error writing custom model content. (nodeRef={0}). +rm.admin.error-client-id=Error generating QName, because client id is already in use. (clientid={0}) +rm.admin.error-split-id=Unable to split id {0}, because separator {1} is not present. \ No newline at end of file diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/messages/audit-service.properties b/rm-server/config/alfresco/module/org_alfresco_module_rm/messages/audit-service.properties new file mode 100644 index 0000000000..6f6d53ded6 --- /dev/null +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/messages/audit-service.properties @@ -0,0 +1,34 @@ +rm.audit.updated-metadata=Updated Metadata +rm.audit.created-object=Created Object +rm.audit.delte-object=Delete Object +rm.audit.login-succeeded=Login Succeeded +rm.audit.login-failed=Login Failed +rm.audit.filed-record=Filed Record +rm.audit.reviewed=Reviewed +rm.audit.cut-off=Cut Off +rm.audit.reversed-cut-off=Reversed Cut Off +rm.audit.destroyed-item=Destroyed Item +rm.audit.opened-record-folder=Opened Record Folder +rm.audit.closed-record-folder=Closed Record Folder +rm.audit.setup-recorder-folder=Setup Recorder Folder +rm.audit.declared-record=Declared Record +rm.audit.undeclared-record=Undeclared Record +rm.audit.froze-item=Froze Item +rm.audit.relinquised-hold=Relinquished Hold +rm.audit.updated-hold-reason=Updated Hold Reason +rm.audit.updated-review-as-of-date=Updated Review As Of Date +rm.audit.updated-disposition-as-of-date=Updated Disposition As Of Date +rm.audit.updated-vital-record-definition=Updated Vital Record Definition +rm.audit.updated-disposition-action-definition=Updated Disposition Action Definition +rm.audit.completed-event=Completed Event +rm.audit.revered-complete-event=Reversed Completed Event +rm.audit.transferred-item=Transferred Item +rm.audit.completed-transfer=Completed Transfer +rm.audit.accession=Accession +rm.audit.copmleted-accession=Completed Accession +rm.audit.scanned-record=Set Record As A Scanned Record +rm.audit.pdf-record=Set Record As PDF A Record +rm.audit.photo-record=Set Record As A Digital Photographic Record +rm.audit.web-record=Set Record As A Web Record +rm.audit.trail-file-fail=Failed to generate audit trail file. +rm.audit.audit-report=Audit Report \ No newline at end of file diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/messages/notification-service.properties b/rm-server/config/alfresco/module/org_alfresco_module_rm/messages/notification-service.properties new file mode 100644 index 0000000000..60d1ffdc22 --- /dev/null +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/messages/notification-service.properties @@ -0,0 +1,2 @@ +notification.dueforreview.subject=Records Due For Review Notification +notification.superseded.subject=Record Superseded Notification \ No newline at end of file diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/messages/records-management-service.properties b/rm-server/config/alfresco/module/org_alfresco_module_rm/messages/records-management-service.properties new file mode 100644 index 0000000000..33b3721073 --- /dev/null +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/messages/records-management-service.properties @@ -0,0 +1,17 @@ +rm.service.error-add-content-container=Content can not be added to a record container. Please use record folders to file content. +rm.service.update-disposition-action-def=Can not update the disposition action definition, because an update is being published. +rm.service.set-id=The identifier property value of the object {0} can not be set, because it is read only. +rm.service.path-node=Unable to get path for node. (nodeRef={0}) +rm.service.invalid-rm-node=Invalid records management node, because aspect {0} is not present. +rm.service.no-root=Unable to find records management root. +rm.service.dup-root=Can not create the records management root, because there is already a records management root in this hierarchy. +rm.service.root-type=Can not create the records management root, because type {0} is not a sub-type of rm:recordsManagementRootContainer. +rm.service.container-parent-type=Can not create records management container, because parent was not sub-type of rm:recordsManagement (parentType={0}) +rm.service.container-type=Can not create records management container, because type {0} is not a sub-type of rm:recordsManagementContainer. +rm.service.container-expected=Node reference to a rm:recordsManagementContainer node expected. +rm.service.record-folder-expected=Node reference to a rm:recordFolder node expected. +rm.service.parent-record-folder-root=Can not create a record folder, because the parent is a records management root. +rm.service.parent-record-folder-type=Can not create record folder, because parent was not sub-type of rm:recordsManagementContainer. (parentType={0}) +rm.service.record-folder-type=Can not create record folder, because provided type is not a sub-type of rm:recordFolder. (type={0}) +rm.service.not-record=The node {0} is not a record. +rm.service.vital-def-missing=Vital record definition aspect is not present on node. (nodeRef={0}) \ No newline at end of file diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/messages/records-model.properties b/rm-server/config/alfresco/module/org_alfresco_module_rm/messages/records-model.properties new file mode 100644 index 0000000000..46622281e4 --- /dev/null +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/messages/records-model.properties @@ -0,0 +1,273 @@ +rma_recordsmanagement.description=Records Management Content Model + +rma_recordsmanagement.type.rma_rmsite.title=Records Management Site +rma_recordsmanagement.type.rma_rmsite.description=Specialised Site for Records Management + +rma_recordsmanagement.type.rma_caveatConfig.title=Caveat Config +rma_recordsmanagement.type.rma_caveatConfig.decription=Caveat Config + +rma_recordsmanagement.type.rma_emailConfig.title=EMail Configuration +rma_recordsmanagement.type.rma_emailConfig.decription=Email Configuration + +rma_recordsmanagement.type.rma_recordsManagementContainer.title=Records Management Container +rma_recordsmanagement.type.rma_recordsManagementContainer.decription=Records Management Container + +rma_recordsmanagement.type.rma_recordsManagementRootContainer.title=Records Management Root Container +rma_recordsmanagement.type.rma_recordsManagementRootContainer.decription=Records Management Root Container + +rma_recordsmanagement.type.rma_dispositionSchedule.title=Disposition Schedule +rma_recordsmanagement.type.rma_dispositionSchedule.decription=Disposition Schedule + +rma_recordsmanagement.property.rma_dispositionAuthority.title=Disposition Authority +rma_recordsmanagement.property.rma_dispositionAuthority.decription=Disposition Authority + +rma_recordsmanagement.property.rma_dispositionInstructions.title=Disposition Instructions +rma_recordsmanagement.property.rma_dispositionInstructions.decription=Disposition Instructions + +rma_recordsmanagement.property.rma_recordLevelDisposition.title=Record Level Disposition +rma_recordsmanagement.property.rma_recordLevelDisposition.decription=Record Level Disposition + +rma_recordsmanagement.association.rma_dispositionActionDefinitions.title=Disposition Actions +rma_recordsmanagement.association.rma_dispositionActionDefinitions.decription=Disposition Actions + +rma_recordsmanagement.type.rma_dispositionActionDefinition.title=Disposition Action Definition +rma_recordsmanagement.type.rma_dispositionActionDefinition.decription=Disposition Action Definition +rma_recordsmanagement.property.rma_dispositionActionName.title=Disposition Action Name +rma_recordsmanagement.property.rma_dispositionActionName.decription=Disposition Action Name +rma_recordsmanagement.property.rma_dispositionDescription.title=Disposition Description +rma_recordsmanagement.property.rma_dispositionDescription.decription=Disposition Description +rma_recordsmanagement.property.rma_dispositionLocation.title=Disposition Location +rma_recordsmanagement.property.rma_dispositionLocation.decription=Disposition Location +rma_recordsmanagement.property.rma_dispositionPeriod.title=Disposition Period +rma_recordsmanagement.property.rma_dispositionPeriod.decription=Disposition Period +rma_recordsmanagement.property.rma_dispositionPeriodProperty.title=Disposition Period Property +rma_recordsmanagement.property.rma_dispositionPeriodProperty.decription=Disposition Period Property +rma_recordsmanagement.property.rma_dispositionEvent.title=Disposition Event +rma_recordsmanagement.property.rma_dispositionEvent.decription=Disposition Event +rma_recordsmanagement.property.rma_dispositionEventCombination.title=Disposition Event Combination +rma_recordsmanagement.property.rma_dispositionEventCombination.decription=Disposition Event Combination + +rma_recordsmanagement.type.rma_recordFolder.title=Record Folder +rma_recordsmanagement.type.rma_recordFolder.decription=Record Folder +rma_recordsmanagement.property.rma_isClosed.title=Record +rma_recordsmanagement.property.rma_isClosed.decription=Record + +rma_recordsmanagement.type.rma_nonElectronicDocument.title=Non-Electronic Document +rma_recordsmanagement.type.rma_nonElectronicDocument.decription=Non-Electronic Document +rma_recordsmanagement.property.rma_physicalSize.title=Physical Size +rma_recordsmanagement.property.rma_physicalSize.decription=The size of the document measured in linear meters +rma_recordsmanagement.property.rma_numberOfCopies.title=Number Of Copies +rma_recordsmanagement.property.rma_numberOfCopies.description=The number of copies of the document +rma_recordsmanagement.property.rma_storageLocation.title=Storage Location +rma_recordsmanagement.property.rma_storageLocation.decription=The physical storage location of the record. +rma_recordsmanagement.property.rma_shelf.title=Shelf +rma_recordsmanagement.property.rma_shelf.decription=The shelf the record resides on. +rma_recordsmanagement.property.rma_box.title=Box +rma_recordsmanagement.property.rma_box.description=The box the record resides in. +rma_recordsmanagement.property.rma_file.title=File +rma_recordsmanagement.property.rma_file.decription=The file the record resides in. + +rma_recordsmanagement.type.rma_dispositionAction.title=Disposition Action +rma_recordsmanagement.type.rma_dispositionAction.decription=Disposition Action +rma_recordsmanagement.property.rma_dispositionActionId.title=Disposition Action Id +rma_recordsmanagement.property.rma_dispositionActionId.decription=Disposition Action Id +rma_recordsmanagement.property.rma_dispositionAction.title=Disposition Action +rma_recordsmanagement.property.rma_dispositionAction.decription=Disposition Action +rma_recordsmanagement.property.rma_dispositionAsOf.title=Disposition Action +rma_recordsmanagement.property.rma_dispositionAsOf.decription=Disposition Action +rma_recordsmanagement.property.rma_dispositionEventsEligible.title=Disposition Events Eligible +rma_recordsmanagement.property.rma_dispositionEventsEligible.decription=Disposition Events Eligible +rma_recordsmanagement.property.rma_dispositionActionStartedAt.title=Disposition Action Started At +rma_recordsmanagement.property.rma_dispositionActionStartedAt.decription=Disposition Action Started At +rma_recordsmanagement.property.rma_dispositionActionStartedBy.title=Disposition Action Started By +rma_recordsmanagement.property.rma_dispositionActionStartedBy.decription=Disposition Action Started By +rma_recordsmanagement.property.rma_dispositionActionCompletedAt.title=Disposition Action Completed At +rma_recordsmanagement.property.rma_dispositionActionCompletedAt.decription=Disposition Action Completed At +rma_recordsmanagement.property.rma_dispositionActionCompletedBy.title=Disposition Action Copmleted By +rma_recordsmanagement.property.rma_dispositionActionCompletedBy.decription=Disposition Action Copmleted By +rma_recordsmanagement.association.rma_eventExecutions.title=Event executions +rma_recordsmanagement.association.rma_eventExecutions.decription=Event executions + +rma_recordsmanagement.type.rma_eventExecution.title=Event Execution +rma_recordsmanagement.type.rma_eventExecution.decription=Event Execution +rma_recordsmanagement.property.rma_eventExecutionName.title=Event Name +rma_recordsmanagement.property.rma_eventExecutionName.decription=Event Name +rma_recordsmanagement.property.rma_eventExecutionAutomatic.title=Event automatic +rma_recordsmanagement.property.rma_eventExecutionAutomatic.decription=Event automatic +rma_recordsmanagement.property.rma_eventExecutionComplete.title=Event complete +rma_recordsmanagement.property.rma_eventExecutionComplete.decription=Event complete +rma_recordsmanagement.property.rma_eventExecutionCompletedBy.title=Event completed by +rma_recordsmanagement.property.rma_eventExecutionCompletedBy.decription=Event completed by +rma_recordsmanagement.property.rma_eventExecutionCompletedAt.title=Event completed at +rma_recordsmanagement.property.rma_eventExecutionCompletedAt.decription=Event completed at + +rma_recordsmanagement.type.rma_hold.title=Hold +rma_recordsmanagement.type.rma_hold.decription=Hold +rma_recordsmanagement.property.rma_holdReason.title=Hold Reason +rma_recordsmanagement.property.rma_holdReason.decription=Hold Reason +rma_recordsmanagement.association.rma_frozenRecords.title=Frozen Records +rma_recordsmanagement.association.rma_frozenRecords.decription=Frozen Records + +rma_recordsmanagement.type.rma_transfer.title=Transfer +rma_recordsmanagement.type.rma_transfer.decription=Transfer +rma_recordsmanagement.property.rma_transferAccessionIndicator.title=Transfer Accession Indicator +rma_recordsmanagement.property.rma_transferAccessionIndicator.decription=Transfer Accession Indicator +rma_recordsmanagement.property.rma_transferPDFIndicator.title=Transfer PDF Indicator +rma_recordsmanagement.property.rma_transferPDFIndicator.decription=Transfer PDF Indicator +rma_recordsmanagement.property.rma_transferLocation.title=Transfer PDF +rma_recordsmanagement.property.rma_transferLocation.decription=Transfer PDF +rma_recordsmanagement.association.rma_transferred.title=Transferred +rma_recordsmanagement.association.rma_transferred.decription=Transferred + +rma_recordsmanagement.aspect.rma_filePlanComponent.title=File Plan Component +rma_recordsmanagement.aspect.rma_filePlanComponent.decription=File Plan Component +rma_recordsmanagement.property.rma_rootNodeRef.title=Root node +rma_recordsmanagement.property.rma_rootNodeRef.decription=Root node + +rma_recordsmanagement.aspect.rma_recordsManagementRoot.title=Records Management Root +rma_recordsmanagement.aspect.rma_recordsManagementRoot.decription=Records Management Root +rma_recordsmanagement.association.rma_holds.title=Holds +rma_recordsmanagement.association.rma_holds.decription=Holds +rma_recordsmanagement.association.rma_transfers.title=Transfers +rma_recordsmanagement.association.rma_transfers.decription=Transfers + +rma_recordsmanagement.aspect.rma_declaredRecord.title=Declared Record +rma_recordsmanagement.aspect.rma_declaredRecord.decription=Declared Record +rma_recordsmanagement.property.rma_declaredAt.title=Date Declared +rma_recordsmanagement.property.rma_declaredAt.decription=Date Declared +rma_recordsmanagement.property.rma_declaredBy.title=Declared By +rma_recordsmanagement.property.rma_declaredBy.decription=Declared By + +rma_recordsmanagement.aspect.rma_recordComponentIdentifier.title=Record component identifier +rma_recordsmanagement.aspect.rma_recordComponentIdentifier.decription=Record component identifier +rma_recordsmanagement.property.rma_identifier.title=Identifier +rma_recordsmanagement.property.rma_identifier.decription=Unique record identifier +rma_recordsmanagement.property.rma_dbUniquenessId.title=Database uniqueness +rma_recordsmanagement.property.rma_dbUniquenessId.decription=Database uniqueness + +rma_recordsmanagement.aspect.rma_vitalRecordDefinition.title=Vital Record Definition +rma_recordsmanagement.aspect.rma_vitalRecordDefinition.decription=Vital Record Definition + +rma_recordsmanagement.property.rma_reviewPeriod.title=Review Period +rma_recordsmanagement.property.rma_reviewPeriod.decription=Review Period +rma_recordsmanagement.property.rma_vitalRecordIndicator.title=Vital Record Indicator +rma_recordsmanagement.property.rma_vitalRecordIndicator.decription=Vital Record Indicator + +rma_recordsmanagement.aspect.rma_record.title=Record +rma_recordsmanagement.aspect.rma_record.decription=Record +rma_recordsmanagement.property.rma_dateFiled.title=Date Filed +rma_recordsmanagement.property.rma_dateFiled.decription=Date Filed +rma_recordsmanagement.property.rma_publicationDate.title=Publication Date +rma_recordsmanagement.property.rma_publicationDate.decription=Publication Date +rma_recordsmanagement.property.rma_originator.title=Originator +rma_recordsmanagement.property.rma_originator.decription=Originator +rma_recordsmanagement.property.rma_originatingOrganization.title=Originating Organization +rma_recordsmanagement.property.rma_originatingOrganization.decription=Originating Organization +rma_recordsmanagement.property.rma_mediaType.title=Media Type +rma_recordsmanagement.property.rma_mediaType.decription=Media Type +rma_recordsmanagement.property.rma_format.title=Format +rma_recordsmanagement.property.rma_format.decription=Format +rma_recordsmanagement.property.rma_dateReceived.title=Date Received +rma_recordsmanagement.property.rma_dateReceived.decription=Date Received +rma_recordsmanagement.property.rma_address.title=Addressee +rma_recordsmanagement.property.rma_address.decription=Addressee +rma_recordsmanagement.property.rma_otherAddress.title=Other Addressee +rma_recordsmanagement.property.rma_otherAddress.decription=Other Addressee + +rma_recordsmanagement.aspect.rma_recordMetaData.title=Record Meta-data +rma_recordsmanagement.aspect.rma_recordMetaData.description=Marker aspect for record meta-data + +rma_recordsmanagement.aspect.rma_commonRecordDetails.title=Common Records Details +rma_recordsmanagement.aspect.rma_commonRecordDetails.description=Meta-data common to all record types +rma_recordsmanagement.property.rma_location.title=Location +rma_recordsmanagement.property.rma_location.decription=Location + +rma_recordsmanagement.aspect.rma_vitalRecord.title=Vital Record +rma_recordsmanagement.aspect.rma_vitalRecord.decription=Vital Record +rma_recordsmanagement.property.rma_reviewAsOf.title=Next Review +rma_recordsmanagement.property.rma_reviewAsOf.decription=Next Review +rma_recordsmanagement.property.rma_notificationIssued.title=Indicates n that this record is due for review has been issued +rma_recordsmanagement.property.rma_notificationIssued.decription=Indicates n that this record is due for review has been issued + +rma_recordsmanagement.aspect.rma_scheduled.title=Scheduled +rma_recordsmanagement.aspect.rma_scheduled.decription=Scheduled +rma_recordsmanagement.association.rma_dispositionSchedule.title=Disposition Schedule +rma_recordsmanagement.association.rma_dispositionSchedule.decription=Disposition Schedule + +rma_recordsmanagement.aspect.rma_dispositionLifecycle.title=Disposition Lifecycle +rma_recordsmanagement.aspect.rma_dispositionLifecycle.decription=Disposition Lifecycle +rma_recordsmanagement.association.rma_nextDispositionAction.title=Next disposition action +rma_recordsmanagement.association.rma_nextDispositionAction.decription=Next disposition action +rma_recordsmanagement.association.rma_dispositionActionHistory.title=Disposition Action History +rma_recordsmanagement.association.rma_dispositionActionHistory.decription=Disposition Action History + +rma_recordsmanagement.aspect.rma_cutOff.title=Cut Off +rma_recordsmanagement.aspect.rma_cutOff.decription=Cut Off +rma_recordsmanagement.property.rma_cutOffDate.title=Cut Off Date +rma_recordsmanagement.property.rma_cutOffDate.decription=Cut Off Date + +rma_recordsmanagement.aspect.rma_transferred.title=Transferred +rma_recordsmanagement.aspect.rma_transferred.decription=Transferred + +rma_recordsmanagement.aspect.rma_ascended.title=Ascended +rma_recordsmanagement.aspect.rma_ascended.decription=Ascended + +rma_recordsmanagement.aspect.rma_frozen.title=Frozen +rma_recordsmanagement.aspect.rma_frozen.decription=Frozen +rma_recordsmanagement.property.rma_frozenAt.title=Frozen At +rma_recordsmanagement.property.rma_frozenAt.decription=Frozen At +rma_recordsmanagement.property.rma_frozenBy.title=Frozen By +rma_recordsmanagement.property.rma_frozenBy.decription=Frozen By + +rma_recordsmanagement.aspect.rma_caveatConfigRoot.title=Caveat Configuration Root +rma_recordsmanagement.aspect.rma_caveatConfigRoot.decription=Caveat Configuration Root +rma_recordsmanagement.association.rma_caveatConfigAssoc.title=Caveat Configuration +rma_recordsmanagement.association.rma_caveatConfigAssoc.description=Caveat Configuration + +rma_recordsmanagement.aspect.rma_emailConfigRoot.title=EmMil Config Root +rma_recordsmanagement.aspect.rma_emailConfigRoot.decription=EMail Config Root +rma_recordsmanagement.association.rma_emailConfigAssoc.title=EMail Configuration +rma_recordsmanagement.association.rma_emailConfigAssoc.description=EMail Configuration + +rma_recordsmanagement.aspect.rma_recordSearch.title=Record Search +rma_recordsmanagement.aspect.rma_recordSearch.decription=Rolled up search information to support Records Management search +rma_recordsmanagement.property.rma_recordSearchHasDispositionSchedule.title=Has Disposition Schedule +rma_recordsmanagement.property.rma_recordSearchHasDispositionSchedule.description=Indicates whether the item has an associated disposition schedule +rma_recordsmanagement.property.rma_recordSearchDispositionActionName.title=Disposition Action Name +rma_recordsmanagement.property.rma_recordSearchDispositionActionName.description=The name of the next disposition action +rma_recordsmanagement.property.rma_recordSearchDispositionActionAsOf.title=Disposition Action Of +rma_recordsmanagement.property.rma_recordSearchDispositionActionAsOf.description=The date at which the next disposation action becomes eligible +rma_recordsmanagement.property.rma_recordSearchDispositionPeriod.title=Disposition Period +rma_recordsmanagement.property.rma_recordSearchDispositionPeriod.description=Disposition Period +rma_recordsmanagement.property.rma_recordSearchDispositionPeriodExpression.title=Disposition Period Expression +rma_recordsmanagement.property.rma_recordSearchDispositionPeriodExpression.description=Disposition Period Expression +rma_recordsmanagement.property.rma_recordSearchDispositionEventsEligible.title=Disposition Events Eligible +rma_recordsmanagement.property.rma_recordSearchDispositionEventsEligible.description=Disposition Events Eligible +rma_recordsmanagement.property.rma_recordSearchDispositionEvents.title=Disposition Events +rma_recordsmanagement.property.rma_recordSearchDispositionEvents.description=Disposition Events +rma_recordsmanagement.property.rma_recordSearchDispositionAuthority.title=Disposition Authority +rma_recordsmanagement.property.rma_recordSearchDispositionAuthority.description=Disposition Authority +rma_recordsmanagement.property.rma_recordSearchDispositionInstructions.title=Disposition Instructions +rma_recordsmanagement.property.rma_recordSearchDispositionInstructions.description=Disposition Instructions +rma_recordsmanagement.property.rma_recordSearchHoldReason.title=Hold Reason +rma_recordsmanagement.property.rma_recordSearchHoldReason.description=Hold Reason +rma_recordsmanagement.property.rma_recordSearchVitalRecordReviewPeriod.title=Vital Record Review Period +rma_recordsmanagement.property.rma_recordSearchVitalRecordReviewPeriod.description=Vital Record Review Period +rma_recordsmanagement.property.rma_recordSearchVitalRecordReviewPeriodExpression.title=Review Period Expression +rma_recordsmanagement.property.rma_recordSearchVitalRecordReviewPeriodExpression.description=Review Period Expression + +rma_recordsmanagement.aspect.rma_versionedRecord.title=Versioned Record +rma_recordsmanagement.aspect.rma_versionedRecord.decription=Versioned Record + +rma_recordsmanagement.aspect.rma_unpublishedUpdate.title=Unpublished Update +rma_recordsmanagement.aspect.rma_unpublishedUpdate.decription=Unpublished Update +rma_recordsmanagement.property.rma_unpublishedUpdate.title=Unpublished Update +rma_recordsmanagement.property.rma_unpublishedUpdate.description=Indicates whether there is an unpublished update +rma_recordsmanagement.property.rma_updateTo.title=Update To +rma_recordsmanagement.property.rma_updateTo.description=Destination of the update +rma_recordsmanagement.property.rma_updatedProperties.title=Updated Properties +rma_recordsmanagement.property.rma_updatedProperties.description=The updated properties +rma_recordsmanagement.property.rma_publishInProgress.title=Publish In Progress +rma_recordsmanagement.property.rma_publishInProgress.description=Indicates whether a publish is currently in progress + + \ No newline at end of file diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/model/recordsModel.xml b/rm-server/config/alfresco/module/org_alfresco_module_rm/model/recordsModel.xml new file mode 100644 index 0000000000..a9a303abb1 --- /dev/null +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/model/recordsModel.xml @@ -0,0 +1,1112 @@ + + + + + + + + + Records Management Model + Roy Wetherall + 1.0 + + + + + + + + + + + + + + + + + + + + + + + RM Site + st:site + + + + + Caveat Config + cm:content + false + + rma:filePlanComponent + + + + + Email Config + cm:content + + rma:filePlanComponent + + + + + + + + Records Management Container + cm:folder + false + + + cm:titled + rma:recordComponentIdentifier + rma:filePlanComponent + + + + + + + Record Category + rma:recordsManagementContainer + + + rma:vitalRecordDefinition + + + + + + + File Plan + rma:recordsManagementContainer + + rma:recordsManagementRoot + + + + + + + + Disposition Schedule + cm:cmobject + + + + + Disposition Authority + d:text + true + + true + false + false + + + + + Disposition Instructions + d:text + true + + + + Record Level Disposition + d:boolean + true + false + + + + + + + + Disposition Actions + + false + false + + + rma:dispositionActionDefinition + false + true + + + + + + + rma:filePlanComponent + + + + + + Disposition Action Definition + cm:cmobject + + + + + Disposition Action Name + d:text + true + + + + Disposition Description + d:text + false + + + + Disposition Location + d:text + false + + true + false + false + + + + + Disposition Period + d:period + false + none|0 + + + + Disposition Period Property + d:text + false + + true + false + false + + + + + Disposition Event + d:text + true + + true + false + false + + + + + Disposition Event Combination + d:text + true + or + + true + false + false + + + + + + rma:filePlanComponent + + + + + + Record Folder + cm:folder + false + + + + + + + + Record Folder Closed + Indicates whether the folder is closed + d:boolean + true + true + false + + + + + + cm:titled + rma:recordComponentIdentifier + rma:commonRecordDetails + rma:filePlanComponent + + + + + + + Non-Electronic Document + cm:content + false + + + + Document Physical Size + d:int + false + + true + false + false + + + + + Number Of Copies + d:int + false + 1 + + true + false + false + + + + + Storage Location + d:text + false + + true + false + false + + + + + Shelf + d:text + false + + true + false + false + + + + + Box + d:text + false + + true + false + false + + + + + File + d:text + false + + true + false + false + + + + + + rma:filePlanComponent + + + + + Disposition Action + cm:cmobject + + + Disposition Action Id + d:text + true + + true + false + false + + + + Disposition Action + d:text + true + + true + false + false + + + + Disposition Action Date + d:date + false + + + Disposition Events Eligible + d:boolean + false + + + Disposition Action Started At + d:date + false + + + Disposition Action Started By + d:text + false + + true + false + false + + + + Disposition Action Completed At + d:date + false + + + Disposition Action Copmleted By + d:text + false + + true + false + false + + + + + + + + + Event executions + + false + false + + + rma:eventExecution + false + true + + + + + + + rma:filePlanComponent + + + + + + + Event Execution + Execution details of an event + cm:cmobject + + + + + Event Name + d:text + true + + true + false + false + + + + Event automatic + d:boolean + true + + + Event complete + d:boolean + true + false + + + Event completed by + d:text + false + + true + false + false + + + + Event completed at + d:date + false + + + + + + rma:filePlanComponent + + + + + + Hold + cm:folder + false + + + + + Hold Reason + d:text + true + + + + + + + + Frozen Records + + false + true + + + rma:filePlanComponent + false + true + + + + + + + cm:titled + rma:filePlanComponent + + + + + + Transfer + cm:folder + false + + + + + Transfer Accession Indicator + d:boolean + true + true + + + + Transfer PDF Indicator + Indicates that transfer includes PDF + d:boolean + true + false + + + + Transfer PDF Indicator + Transfer Location + d:text + + + + + + + + Transferred + + false + false + + + rma:dispositionLifecycle + false + true + + + + + + + cm:titled + rma:filePlanComponent + + + + + + + + + + + + + File Plan Component + false + + + Root node reference + d:noderef + + + + + + Records Management Root + + + + Holds + + false + false + + + rma:hold + false + true + + + + + + Transfers + + false + false + + + rma:transfer + false + true + + + + + + rma:filePlanComponent + + + + + Declared Record + + + Date Declared + d:date + + + Declared By + d:text + + true + false + false + + + + + rma:filePlanComponent + + + + + Record component identifier + + + Record Component Identifier + d:text + true + + true + false + false + + + + Database uniqueness id + d:text + true + false + + + + rma:filePlanComponent + + + + + Vital Record Definition + + + Review Period + d:period + true + none|0 + + + Vital Record Indicator + d:boolean + true + false + + + + rma:filePlanComponent + + + + + + + Record + + false + + + + + + + + Date Filed + d:date + true + + + + Publication Date + d:date + true + + + + Originator + d:text + true + + true + false + false + + + + + Originating Organization + d:text + true + + true + false + false + + + + + Media Type + d:text + false + + true + false + false + + + + + Format + d:text + false + + true + false + false + + + + + Date Received + d:date + false + + + + + Addressee + d:text + false + + true + false + false + + + + Other Addressee + d:text + false + + true + false + false + + + + + + + cm:titled + rma:recordComponentIdentifier + rma:commonRecordDetails + rma:filePlanComponent + + + + + + + + + + + + Location + d:text + false + + true + false + false + + + + + + rma:filePlanComponent + + + + + + + Vital Record + + + Next Review Date + d:date + false + + + Indicates whether a notification that this record is due for review has been issued + d:boolean + false + false + + + + rma:filePlanComponent + + + + + Scheduled + + + + Disposition Schedule + + false + false + + + rma:dispositionSchedule + false + false + + + + + + rma:filePlanComponent + + + + + Disposition Lifecycle + + + + Next disposition action + + false + false + + + rma:dispositionAction + false + false + + + + + + Disposition Action History + + false + false + + + rma:dispositionAction + false + true + + + + + + rma:filePlanComponent + + + + + + Cut Off + + + Cut Off Date + d:date + true + + + + + + + Transferred + + + + + Ascended + + + + Frozen + + + Frozen At Date + d:date + true + + + Frozen By + d:text + true + + true + false + false + + + + + rma:filePlanComponent + + + + + Caveat Config Root + + + + true + false + + + rma:caveatConfig + false + false + + false + + + + + + + Email Config Root + + + + true + false + + + rma:emailConfig + false + false + + false + + + + + + + + Record Search + + + d:boolean + + + d:text + + true + false + false + + + + d:date + + + d:text + + true + false + false + + + + d:text + + true + false + false + + + + d:boolean + + + d:text + true + + true + false + false + + + + d:text + + true + false + false + + + + d:text + + + d:text + + + d:text + + true + false + false + + + + d:text + + true + false + false + + + + + + + Versioned Record + + + + Unpublished Update + + + d:boolean + true + true + + + d:text + + + d:any + + + d:boolean + true + false + + + + + + + Ghosted Record + false + + + + + \ No newline at end of file diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/model/recordsPermissionModel.xml b/rm-server/config/alfresco/module/org_alfresco_module_rm/model/recordsPermissionModel.xml new file mode 100644 index 0000000000..e6f1383dce --- /dev/null +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/model/recordsPermissionModel.xml @@ -0,0 +1,490 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/module-context.xml b/rm-server/config/alfresco/module/org_alfresco_module_rm/module-context.xml new file mode 100644 index 0000000000..e475dac07e --- /dev/null +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/module-context.xml @@ -0,0 +1,219 @@ + + + + + + + + + + + + + + + + + + + /${spaces.company_home.childname}/${spaces.dictionary.childname} + alfresco/module/org_alfresco_module_rm/bootstrap/RMDataDictionaryBootstrap.xml + + + + + + + + + + alfresco.module.org_alfresco_module_rm.messages.notification-service + alfresco.module.org_alfresco_module_rm.messages.admin-service + alfresco.module.org_alfresco_module_rm.messages.records-management-service + alfresco.module.org_alfresco_module_rm.messages.action-service + alfresco.module.org_alfresco_module_rm.messages.audit-service + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + rmService + + + + + + + + + + + alfresco.module.org_alfresco_module_rm.rm-events + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + caveatConfig + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + EEE, d MMM yyyy HH:mm:ss Z + EEE, d MMM yy HH:mm:ss Z + + + + + + + + + + + + + diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/module.properties b/rm-server/config/alfresco/module/org_alfresco_module_rm/module.properties new file mode 100644 index 0000000000..c5dde346e8 --- /dev/null +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/module.properties @@ -0,0 +1,11 @@ +# Alfresco Records Management Module +module.id=org_alfresco_module_rm + +# 23/02/2012 - Renamed +module.aliases=org_alfresco_module_dod5015 + +module.title=Records Management +module.description=Alfresco Record Management Extension +module.version=2.0 + +module.repo.version.min=4.0 \ No newline at end of file diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-action-context.xml b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-action-context.xml new file mode 100644 index 0000000000..9fe28aa465 --- /dev/null +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-action-context.xml @@ -0,0 +1,811 @@ + + + + + + + + + + alfresco.module.org_alfresco_module_rm.rm-actions + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction + org.alfresco.repo.action.executer.ActionExecuter + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_ALLOW + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.*=RM_ALLOW + org.alfresco.repo.action.executer.ActionExecuter.*=RM_ALLOW + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_CAP.0.rma:filePlanComponent.ApproveRecordsScheduledForCutoff + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.*=RM_ALLOW + org.alfresco.repo.action.executer.ActionExecuter.*=RM_ALLOW + + + + + + + + + + + + + + + + + + + + + + + + + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_CAP.0.rma:filePlanComponent.ApproveRecordsScheduledForCutoff + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.*=RM_ALLOW + org.alfresco.repo.action.executer.ActionExecuter.*=RM_ALLOW + + + + + + + + + + + + + + + + + + + + + + + + + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_CAP.0.rma:filePlanComponent.DestroyRecordsScheduledForDestruction + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.*=RM_ALLOW + org.alfresco.repo.action.executer.ActionExecuter.*=RM_ALLOW + + + + + + + + + + + ${rm.ghosting.enabled} + + + + + + + + + + + + + + + + + + + + + + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_ALLOW + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.*=RM_ALLOW + org.alfresco.repo.action.executer.ActionExecuter.*=RM_ALLOW + + + + + + + + + + + + + + + + + + + + + + + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_CAP.0.rma:filePlanComponent.ReOpenFolders + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.*=RM_ALLOW + org.alfresco.repo.action.executer.ActionExecuter.*=RM_ALLOW + + + + + + + + + + + + + + + + + + + + + + + + + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_CAP.0.rma:filePlanComponent.CloseFolders + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.*=RM_ALLOW + org.alfresco.repo.action.executer.ActionExecuter.*=RM_ALLOW + + + + + + + + + + + + + + + + + + + + + + + + + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_CAP.0.rma:filePlanComponent.CycleVitalRecords + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.*=RM_ALLOW + org.alfresco.repo.action.executer.ActionExecuter.*=RM_ALLOW + + + + + + + + + + + + + + + + + + + + + + + + + + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_ALLOW + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.*=RM_ALLOW + org.alfresco.repo.action.executer.ActionExecuter.*=RM_ALLOW + + + + + + + + + + + + + + + + + + + + + + + + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM.Declare.0 + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.*=RM_ALLOW + org.alfresco.repo.action.executer.ActionExecuter.*=RM_ALLOW + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_CAP.0.rma:filePlanComponent.UndeclareRecords + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.*=RM_ALLOW + org.alfresco.repo.action.executer.ActionExecuter.*=RM_ALLOW + + + + + + + + + + + + + + + + + + + + + + + + + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_CAP.0.rma:filePlanComponent.ExtendRetentionPeriodOrFreeze + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.*=RM_ALLOW + org.alfresco.repo.action.executer.ActionExecuter.*=RM_ALLOW + + + + + + + + + + + + + + + + + + + + + + + + + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_CAP.0.rma:filePlanComponent.Unfreeze + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.*=RM_ALLOW + org.alfresco.repo.action.executer.ActionExecuter.*=RM_ALLOW + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_CAP.0.rma:filePlanComponent.ViewUpdateReasonsForFreeze + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.*=RM_ALLOW + org.alfresco.repo.action.executer.ActionExecuter.*=RM_ALLOW + + + + + + + + + + + + + + + + + + + + + + + + + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_CAP.0.rma:filePlanComponent.PlanningReviewCycles + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.*=RM_ALLOW + org.alfresco.repo.action.executer.ActionExecuter.*=RM_ALLOW + + + + + + + + + + + + + + + + + + + + + + + + + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_CAP.0.rma:filePlanComponent.ManuallyChangeDispositionDates + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.*=RM_ALLOW + org.alfresco.repo.action.executer.ActionExecuter.*=RM_ALLOW + + + + + + + + + + + + + + + + + + + + + + + + + + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_ALLOW + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.*=RM_ALLOW + org.alfresco.repo.action.executer.ActionExecuter.*=RM_ALLOW + + + + + + + + + + + + + + + + + + + + + + + + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_ALLOW + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.*=RM_ALLOW + org.alfresco.repo.action.executer.ActionExecuter.*=RM_ALLOW + + + + + + + + + + + + + + + + + + + + + + + + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_CAP.0.rma:filePlanComponent.AddModifyEventDates + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.*=RM_ALLOW + org.alfresco.repo.action.executer.ActionExecuter.*=RM_ALLOW + + + + + + + + + + + + + + + + + + + + + + + + + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_CAP.0.rma:filePlanComponent.AddModifyEventDates + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.*=RM_ALLOW + org.alfresco.repo.action.executer.ActionExecuter.*=RM_ALLOW + + + + + + + + + + + + + + + + + + + + + + + + + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_CAP.0.rma:filePlanComponent.AuthorizeAllTransfers + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.*=RM_ALLOW + org.alfresco.repo.action.executer.ActionExecuter.*=RM_ALLOW + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_CAP.0.rma:filePlanComponent.AuthorizeNominatedTransfers + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.*=RM_ALLOW + org.alfresco.repo.action.executer.ActionExecuter.*=RM_ALLOW + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.execute=RM_CAP.0.rma:filePlanComponent.CreateModifyDestroyFileplanMetadata + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction.*=RM_ALLOW + org.alfresco.repo.action.executer.ActionExecuter.*=RM_ALLOW + + + + + + + + + + \ No newline at end of file diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-actions.properties b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-actions.properties new file mode 100644 index 0000000000..47a031717d --- /dev/null +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-actions.properties @@ -0,0 +1,88 @@ +# Disposition Actions + +cutoff.title=Cutoff +cutoff.description=Cutoff + +retain.title=Retain +retain.description=Retain + +destroy.title=Destroy +destroy.description=Destroy + +# Records Management Actions + +file.title=File +file.description=File + +reviewed.title=Reviewed +reviewed.description=Reviewed + +openRecordFolder.title=Open Folder +openRecordFolder.description=Open Folder + +closeRecordFolder.title=Close Folder +closeRecordFolder.description=Close Folder + +setupRecordFolder.title=Setup Folder +setupRecordFolder.description=Setup Folder + +declareRecord.title=Declare Record +declareRecord.description=Declare Record + +undeclareRecord.title=Undeclare Record +undeclareRecord.description=Undeclare Record + +freeze.title=Freeze +freeze.description=Freeze + +unfreeze.title=Unfreeze +unfreeze.description=Unfreeze + +relinquishHold.title=Relinquish Hold +relinquishHold.description=Relinquish Hold + +broadcastVitalRecordDefinition.title=Broadcast Vital Record Definition +broadcastVitalRecordDefinition.description=Broadcast Vital Record Definition + +broadcastDispositionActionDefinitionUpdate.title=Broadcast Disposition Action Definition Update +broadcastDispositionActionDefinitionUpdate.description=Broadcast Disposition Action Definition Update + + +completeEvent.title=Complete Event +completeEvent.description=Complete Event + +undoEvent.title=Undo Event +undoEvent.description=Undo Event + +applyScannedRecord.title=Apply Scanned Record +applyScannedRecord.description=Apply Scanned Record + +applyPdfRecord.title=Apply PDF Record +applyPdfRecord.description=Apply PDF Record + +applyDigitalPhotographRecord.title=Apply Digital Photograph Record +applyDigitalPhotographRecord.description=Apply Digital Photograph Record + +applyWebRecord.title=Apply Web Record +applyWebRecord.description=Apply Web Record + +splitEmail.title=Split Email Attatchments +splitEmail.description=Split email attachments into separate records. + +transfer.title=Transfer +transfer.description=Transfer + +accession.title=Accession +accession.description=Accession + +editHoldReason.title=Edit Hold Reason +editHoldReason.description=Edit Hold Reason + +editReviewAsOfDate.title=Edit Review Date +editReviewAsOfDate.description=Edit Review Date + +editDispositionActionAsOfDate.title=Edit Disposition Action As Of Date +editDispositionActionAsOfDate.description=Edit Disposition Action As Of Date + +createDispositionSchedule.title=Create Disposition Schedule +createDispositionSchedule.description=Create Disposition Schedule diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-capabilities-context.xml b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-capabilities-context.xml new file mode 100644 index 0000000000..dce641860e --- /dev/null +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-capabilities-context.xml @@ -0,0 +1,861 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + RECORD_FOLDER + RECORD + + + + + + + + + + + + + + + + + RECORD_FOLDER + RECORD + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + RECORD_FOLDER + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + FILE_PLAN + RECORD_CATEGORY + DISPOSITION_SCHEDULE + + + + + + + + + + + + + + + RECORD_CATEGORY + RECORD_FOLDER + RECORD + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + RECORD_FOLDER + RECORD + + + + + + + + + + + + + + + + + + RECORD_FOLDER + RECORD + + + + + + + + + + + + + + + + + + + + + + + + RECORD + + + + + + + + + + + + + + + + + + + RECORD + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + RECORD + + + + + + + + + + + + + + + RECORD_FOLDER + RECORD + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + RECORD + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + RECORD + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + RECORD_FOLDER + RECORD + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + RECORD_FOLDER + + + + + + + + + + + + + + + + + + + + + + + RECORD_FOLDER + RECORD + + + + + + + + + + + + + + + + + RECORD + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + RECORD + + + + + + + + + + + + + + \ No newline at end of file diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-events.properties b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-events.properties new file mode 100644 index 0000000000..354503dbdd --- /dev/null +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-events.properties @@ -0,0 +1,5 @@ +# Event Types +rmeventservice.rmEventType.simple=Simple Event +rmeventservice.rmEventType.obsolete=Obsoleted Event +rmeventservice.rmEventType.superseded=Superseded Event +rmeventservice.rmEventType.crossReferencedRecordTransfered=Cross Referenced Record Transfered \ No newline at end of file diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-id-context.xml b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-id-context.xml new file mode 100644 index 0000000000..fd1b8662a6 --- /dev/null +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-id-context.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-job-context.xml b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-job-context.xml new file mode 100644 index 0000000000..80d7c95f95 --- /dev/null +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-job-context.xml @@ -0,0 +1,134 @@ + + + + + + + + + + + org.alfresco.module.org_alfresco_module_rm.job.NotifyOfRecordsDueForReviewJob + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 0/15 * * * ? + + + + + + + org.alfresco.module.org_alfresco_module_rm.job.DispositionLifecycleJob + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 0/15 * * * ? + + + + + + org.alfresco.module.org_alfresco_module_rm.job.PublishUpdatesJob + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0/30 * * * * ? + + + + + + + + + + + + + \ No newline at end of file diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-model-context.xml b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-model-context.xml new file mode 100644 index 0000000000..2b018359af --- /dev/null +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-model-context.xml @@ -0,0 +1,103 @@ + + + + + + + + + + alfresco/module/org_alfresco_module_rm/model/recordsModel.xml + + + + + alfresco/module/org_alfresco_module_rm/messages/records-model + + + + + + /app:company_home/app:dictionary/cm:records_management + + + path + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-patch-context.xml b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-patch-context.xml new file mode 100644 index 0000000000..faf01ef9d4 --- /dev/null +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-patch-context.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-public-services-security-context.xml b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-public-services-security-context.xml new file mode 100644 index 0000000000..4eecb97413 --- /dev/null +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-public-services-security-context.xml @@ -0,0 +1,922 @@ + + + + + + + + + + + + + + + + + + + + + alfresco/model/permissionDefinitions.xml + + + alfresco/model/permissionSchema.dtd + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {http://www.alfresco.org/model/recordsmanagement/1.0}filePlanComponent + + + + + + + + + + + + + + + + + + + + + + + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${system.acl.maxPermissionCheckTimeMillis} + + + ${system.acl.maxPermissionChecks} + + + + {http://www.alfresco.org/model/recordsmanagement/1.0}filePlanComponent + + + + + + + ${system.acl.maxPermissionCheckTimeMillis} + + + ${system.acl.maxPermissionChecks} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-service-context.xml b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-service-context.xml new file mode 100644 index 0000000000..a5ba27e4bf --- /dev/null +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-service-context.xml @@ -0,0 +1,1014 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + org.alfresco.module.org_alfresco_module_rm.RecordsManagementService + + + + + + + + + + + + + + + + + + + + ${server.transaction.mode.default} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.alfresco.module.org_alfresco_module_rm.vital.VitalRecordService + + + + + + + + + + + + + + + + + + + + ${server.transaction.mode.default} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService + + + + + + + + + + + + + + + + + + + + ${server.transaction.mode.default} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.alfresco.module.org_alfresco_module_rm.search.RecordsManagementSearchService + + + + + + + + + + + + + + + + + + + + ${server.transaction.mode.default} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Create + UpdateProperties + ManageAccessRights + Delete + AccessAudit + CycleVitalRecords + ApproveRecordsScheduledForCutoff + DestroyRecordsCapability + DestroyRecordsScheduledForDestruction + AuthorizeAllTransfers + AuthorizeNominatedTransfers + CreateModifyRecordsInCutoffFolders + ManuallyChangeDispositionDates + PlanningReviewCycles + UndeclareRecords + Declare + Unfreeze + ViewUpdateReasonsForFreeze + CloseFolders + ReOpenFolders + ExtendRetentionPeriodOrFreeze + + + + + + + + + org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService + + + + + + + + + + + + + + + + + + + + ${server.transaction.mode.default} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService + + + + + + + + + + + + + + + + + + + + ${server.transaction.mode.default} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEventService + + + + + + + + + + + + + + + + + + + + ${server.transaction.mode.default} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + rma:recordCategory + rma:recordFolder + rma:record + rma:nonElectronicDocument + + + + + + + org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminService + + + + + + + + + + + + + + + + + + + + ${server.transaction.mode.default} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.alfresco.module.org_alfresco_module_rm.audit.RecordsManagementAuditService + + + + + + + + + + + + + + + + + + + + ${server.transaction.mode.default} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${server.transaction.mode.default} + + + + + + + + + + + + + + + + + + + + + + + + + + org.alfresco.module.org_alfresco_module_rm.email.CustomEmailMappingService + + + + customEmailMappingService + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {http://www.alfresco.org/model/recordsmanagement/1.0}recordComponentIdentifier + + + + + + + + {http://www.alfresco.org/model/rmcustom/1.0}rmcustom + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${server.transaction.mode.default} + + + + + + + + org.alfresco.module.org_alfresco_module_rm.caveat.RMCaveatConfigService + + + + caveatConfigService + + + + + + + + + + + + + + + + {http://www.alfresco.org/model/recordsmanagement/1.0}cutOffDate + {http://www.alfresco.org/model/recordsmanagement/1.0}dispositionAsOf + {http://www.alfresco.org/model/recordsmanagement/1.0}dateFiled + {http://www.alfresco.org/model/recordsmanagement/1.0}publicationDate + + + + + \ No newline at end of file diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-ui-evaluators-context.xml b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-ui-evaluators-context.xml new file mode 100644 index 0000000000..18d3b99aff --- /dev/null +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-ui-evaluators-context.xml @@ -0,0 +1,600 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + RECORD_FOLDER + + + + + + + + + + RECORD_FOLDER + + + + + + + + + RECORD + + + + + + + + + RECORD_FOLDER + + + + + + + + + RECORD_FOLDER + RECORD + + + + + + + + + RECORD_FOLDER + RECORD + + + + + + + + + + RECORD_FOLDER + RECORD + + + + + + + + + + RECORD_FOLDER + RECORD + + + + + + + + + + RECORD_FOLDER + RECORD + + + + + + + + + + RECORD_FOLDER + RECORD + + + + + + + + + RECORD_FOLDER + RECORD + + + + + + + + + + RECORD_FOLDER + RECORD + + + + + + + + + + RECORD + + + + + + + + + + RECORD + + + + + + + + + + + RECORD_CATEGORY + RECORD_FOLDER + RECORD + + + + + + + + + + RECORD_CATEGORY + RECORD_FOLDER + RECORD + + + + + + + + + + RECORD_CATEGORY + RECORD_FOLDER + RECORD + + + + + + + + + + RECORD_FOLDER + + + + + + + + + + RECORD_FOLDER + + + + + + + + + + RECORD_FOLDER + + + + + + + + + + + RECORD_FOLDER + RECORD + + + + + + + + + + RECORD_FOLDER + RECORD + + + + + + + + + + RECORD_FOLDER + RECORD + + + + + + + + + + RECORD_FOLDER + RECORD + + + + + + + + + + + RECORD_FOLDER + RECORD + + + + + + + + + + RECORD_FOLDER + RECORD + + + + + + + + + + RECORD_FOLDER + RECORD + + + + + + + + + + RECORD_FOLDER + RECORD + + + + + + + + + + RECORD_FOLDER + RECORD + + + + + + + + + RECORD_FOLDER + + + + + + + + + RECORD + + + + + + + + + + RECORD + + + + + + + + + + RECORD + + + + + + + + + + RECORD + + + + + + + + + + FILE_PLAN + RECORD_CATEGORY + + + + + + + + + + RECORD_CATEGORY + + + + + + + + + + + + + + + + + + + + HOLD + + + + + + + + + + HOLD + + + + + + + + + + + + + + + + + + TRANSFER + + + + + + + + + + TRANSFER + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + RECORD + + + + + + + + + + RECORD + + + + + + + + + + RECORD + + + + + + + + + + RECORD + + + + + diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-webscript-context.xml b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-webscript-context.xml new file mode 100644 index 0000000000..0b06db0dcb --- /dev/null +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-webscript-context.xml @@ -0,0 +1,344 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/security/rm-default-roles-bootstrap.json b/rm-server/config/alfresco/module/org_alfresco_module_rm/security/rm-default-roles-bootstrap.json new file mode 100644 index 0000000000..65687c61d7 --- /dev/null +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/security/rm-default-roles-bootstrap.json @@ -0,0 +1,187 @@ +[ + { + "name" : "User", + "displayLabel" : "Records Management User", + "isAdmin" : false, + "capabilities" : + [ + "DeclareRecords", + "ViewRecords" + ] + }, + { + "name" : "PowerUser", + "displayLabel" : "Records Management Power User", + "isAdmin" : false, + "capabilities" : + [ + "DeclareRecords", + "ViewRecords", + "CreateModifyDestroyFolders", + "EditRecordMetadata", + "EditNonRecordMetadata", + "AddModifyEventDates", + "CloseFolders", + "DeclareRecordsInClosedFolders", + "ReOpenFolders", + "CycleVitalRecords", + "PlanningReviewCycles" + ] + }, + { + "name" : "SecurityOfficer", + "displayLabel" : "Records Management Security Officer", + "isAdmin" : false, + "capabilities" : + [ + "DeclareRecords", + "ViewRecords", + "CreateModifyDestroyFolders", + "EditRecordMetadata", + "EditNonRecordMetadata", + "AddModifyEventDates", + "CloseFolders", + "DeclareRecordsInClosedFolders", + "ReOpenFolders", + "CycleVitalRecords", + "PlanningReviewCycles", + "UpdateClassificationDates", + "CreateModifyDestroyClassificationGuides", + "UpgradeDowngradeAndDeclassifyRecords", + "UpdateExemptionCategories" + ] + }, + { + "name" : "RecordsManager", + "displayLabel" : "Records Management Records Manager", + "isAdmin" : false, + "capabilities" : + [ + "DeclareRecords", + "ViewRecords", + "CreateModifyDestroyFolders", + "EditRecordMetadata", + "EditNonRecordMetadata", + "AddModifyEventDates", + "CloseFolders", + "DeclareRecordsInClosedFolders", + "ReOpenFolders", + "CycleVitalRecords", + "PlanningReviewCycles", + "UpdateTriggerDates", + "CreateModifyDestroyEvents", + "ManageAccessRights", + "MoveRecords", + "ChangeOrDeleteReferences", + "DeleteLinks", + "EditDeclaredRecordMetadata", + "ManuallyChangeDispositionDates", + "ApproveRecordsScheduledForCutoff", + "CreateModifyRecordsInCutoffFolders", + "ExtendRetentionPeriodOrFreeze", + "Unfreeze", + "ViewUpdateReasonsForFreeze", + "DestroyRecordsScheduledForDestruction", + "DestroyRecords", + "UpdateVitalRecordCycleInformation", + "UndeclareRecords", + "DeclareAuditAsRecord", + "DeleteAudit", + "CreateModifyDestroyTimeframes", + "AuthorizeNominatedTransfers", + "EditSelectionLists", + "AuthorizeAllTransfers", + "CreateModifyDestroyFileplanMetadata", + "CreateAndAssociateSelectionLists", + "AttachRulesToMetadataProperties", + "CreateModifyDestroyFileplanTypes", + "CreateModifyDestroyRecordTypes", + "MakeOptionalParametersMandatory", + "MapEmailMetadata", + "DeleteRecords", + "TriggerAnEvent", + "CreateModifyDestroyRoles", + "CreateModifyDestroyUsersAndGroups", + "PasswordControl", + "EnableDisableAuditByTypes", + "SelectAuditMetadata", + "DisplayRightsReport", + "AccessAudit", + "ExportAudit", + "CreateModifyDestroyReferenceTypes", + "UpdateClassificationDates", + "CreateModifyDestroyClassificationGuides", + "UpgradeDowngradeAndDeclassifyRecords", + "UpdateExemptionCategories", + "MapClassificationGuideMetadata" + + ] + }, + { + "name" : "Administrator", + "displayLabel" : "Records Management Administrator", + "isAdmin" : true, + "capabilities" : + [ + "DeclareRecords", + "ViewRecords", + "CreateModifyDestroyFolders", + "EditRecordMetadata", + "EditNonRecordMetadata", + "AddModifyEventDates", + "CloseFolders", + "DeclareRecordsInClosedFolders", + "ReOpenFolders", + "CycleVitalRecords", + "PlanningReviewCycles", + "UpdateTriggerDates", + "CreateModifyDestroyEvents", + "ManageAccessRights", + "MoveRecords", + "ChangeOrDeleteReferences", + "DeleteLinks", + "EditDeclaredRecordMetadata", + "ManuallyChangeDispositionDates", + "ApproveRecordsScheduledForCutoff", + "CreateModifyRecordsInCutoffFolders", + "ExtendRetentionPeriodOrFreeze", + "Unfreeze", + "ViewUpdateReasonsForFreeze", + "DestroyRecordsScheduledForDestruction", + "DestroyRecords", + "UpdateVitalRecordCycleInformation", + "UndeclareRecords", + "DeclareAuditAsRecord", + "DeleteAudit", + "CreateModifyDestroyTimeframes", + "AuthorizeNominatedTransfers", + "EditSelectionLists", + "AuthorizeAllTransfers", + "CreateModifyDestroyFileplanMetadata", + "CreateAndAssociateSelectionLists", + "AttachRulesToMetadataProperties", + "CreateModifyDestroyFileplanTypes", + "CreateModifyDestroyRecordTypes", + "MakeOptionalParametersMandatory", + "MapEmailMetadata", + "DeleteRecords", + "TriggerAnEvent", + "CreateModifyDestroyRoles", + "CreateModifyDestroyUsersAndGroups", + "PasswordControl", + "EnableDisableAuditByTypes", + "SelectAuditMetadata", + "DisplayRightsReport", + "AccessAudit", + "ExportAudit", + "CreateModifyDestroyReferenceTypes", + "UpdateClassificationDates", + "CreateModifyDestroyClassificationGuides", + "UpgradeDowngradeAndDeclassifyRecords", + "UpdateExemptionCategories", + "MapClassificationGuideMetadata", + "ManageAccessControls" + ] + } +] + diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/emailmap.get.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/emailmap.get.desc.xml new file mode 100644 index 0000000000..0347abaa75 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/emailmap.get.desc.xml @@ -0,0 +1,29 @@ + + Get the custom email property map + + fields are specified with "from" and "to". +
+ Example data. +
+    "mappings":
+    [ 
+       {"from" : "messageTo", "to" : "imap:messageTo" } , 
+       {"from" : "Thread-Index", "to" : "imap:threadIndex" } , 
+       {"from" : "messageFrom", "to" : "imap:messageFrom" } , 
+       {"from" : "messageSubject", "to" : "cm:title" } , 
+       {"from" : "messageSubject", "to" : "imap:messageSubject" } , 
+       {"from" : "messageSubject", "to" : "cm:description" } , 
+       {"from" : "messageCc", "to" : "imap:messageCc" } , 
+       {"from" : "Message-ID", "to" : "imap:messageId" } 
+    ] 
+  
+ ]]> +
+ /api/rma/admin/emailmap + argument + user + required + internal +
\ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/emailmap.get.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/emailmap.get.json.ftl new file mode 100644 index 0000000000..f38906ee62 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/emailmap.get.json.ftl @@ -0,0 +1,8 @@ +<#import "emailmap.lib.ftl" as emailmapLib/> + +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "data": + <@emailmapLib.emailmapJSON emailmap=emailmap /> +} + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/emailmap.lib.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/emailmap.lib.ftl new file mode 100644 index 0000000000..d4a297276e --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/emailmap.lib.ftl @@ -0,0 +1,14 @@ +<#-- renders an email map object --> + +<#macro emailmapJSON emailmap> +<#escape x as jsonUtils.encodeJSONString(x)> + { + "mappings": + [ + <#list emailmap as mapping> + {"from": "${mapping.from}", "to": "${mapping.to}" }<#if mapping_has_next>, + + ] + } + + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/emailmap.post.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/emailmap.post.desc.xml new file mode 100644 index 0000000000..eec6580c60 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/emailmap.post.desc.xml @@ -0,0 +1,40 @@ + + Update email property map + + Data is specified in JSON format as a JSONObject with two optional fields, "add" and "delete". +
+ The contents of the add array are added. +
+ The contents of the delete array are deleted. +
+ Add mapping: +
+   {
+      "add":
+      [
+         {"to":"rmc:Wibble", "from":"whatever"},
+         {"to":"rmc:wobble", "from":"whatever"}
+      ]
+   }
+  
+ Delete mapping: +
+   {
+      "delete":
+      [
+         {"to":"rmc:Wibble", "from":"whatever"},
+         {"to":"rmc:wobble", "from":"whatever"}
+      ]
+   }
+  
+ Returns data in the same format as the get method + ]]> +
+ /api/rma/admin/emailmap + argument + user + required + internal +
\ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/emailmap.post.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/emailmap.post.json.ftl new file mode 100644 index 0000000000..68bf020fe0 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/emailmap.post.json.ftl @@ -0,0 +1,7 @@ +<#import "emailmap.lib.ftl" as emailmapLib/> + +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "data": <@emailmapLib.emailmapJSON emailmap=emailmap /> +} + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/emailmap.put.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/emailmap.put.desc.xml new file mode 100644 index 0000000000..eec6580c60 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/emailmap.put.desc.xml @@ -0,0 +1,40 @@ + + Update email property map + + Data is specified in JSON format as a JSONObject with two optional fields, "add" and "delete". +
+ The contents of the add array are added. +
+ The contents of the delete array are deleted. +
+ Add mapping: +
+   {
+      "add":
+      [
+         {"to":"rmc:Wibble", "from":"whatever"},
+         {"to":"rmc:wobble", "from":"whatever"}
+      ]
+   }
+  
+ Delete mapping: +
+   {
+      "delete":
+      [
+         {"to":"rmc:Wibble", "from":"whatever"},
+         {"to":"rmc:wobble", "from":"whatever"}
+      ]
+   }
+  
+ Returns data in the same format as the get method + ]]> +
+ /api/rma/admin/emailmap + argument + user + required + internal +
\ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/emailmap.put.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/emailmap.put.json.ftl new file mode 100644 index 0000000000..99d37fbb76 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/emailmap.put.json.ftl @@ -0,0 +1,8 @@ +<#import "emailmap.lib.ftl" as emailmapLib/> + +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "data": + <@emailmapLib.emailmapJSON emailmap=emailmap /> +} + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraint.delete.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraint.delete.desc.xml new file mode 100644 index 0000000000..0dd4d5018f --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraint.delete.desc.xml @@ -0,0 +1,13 @@ + + Delete an RM Constraint list + + + + /api/rma/admin/rmconstraints/{listName} + argument + user + required + internal + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraint.delete.js b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraint.delete.js new file mode 100644 index 0000000000..3863fdf3b5 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraint.delete.js @@ -0,0 +1,27 @@ +/** + * Delete the rm constraint list + */ +function main() +{ + // Get the shortname + var shortName = url.extension; + + // Get the constraint + var constraint = caveatConfig.getConstraint(shortName); + + if (constraint != null) + { + caveatConfig.deleteConstraintList(shortName); + + // Pass the constraint name to the template + model.constraintName = shortName; + } + else + { + // Return 404 + status.setCode(404, "Constraint List " + shortName + " does not exist"); + return; + } +} + +main(); \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraint.delete.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraint.delete.json.ftl new file mode 100644 index 0000000000..71ff31ba48 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraint.delete.json.ftl @@ -0,0 +1,5 @@ +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "data": { } +} + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraint.get.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraint.get.desc.xml new file mode 100644 index 0000000000..db15d75311 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraint.get.desc.xml @@ -0,0 +1,22 @@ + + Get a RM Constraint method + + + Constraint object +
+
constraintName
the name of the constraint. The underscore character is used instead of the colon
+
constraintTitle
the title of the constraint (human readable)
+
caseSensitive
Are the values case sensitive
+
allowedValues
array of allowed values, this is the complete unrestricted list of all values
+
+ ]]> +
+ /api/rma/admin/rmconstraints/{listName} + argument + user + required + internal +
\ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraint.get.js b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraint.get.js new file mode 100644 index 0000000000..3abedfd921 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraint.get.js @@ -0,0 +1,25 @@ +/** + * Get the detail of the rm constraint + */ +function main() +{ + // Get the shortname + var shortName = url.extension; + + // Get the constraint + var constraint = caveatConfig.getConstraint(shortName); + + if (constraint != null) + { + // Pass the constraint detail to the template + model.constraint = constraint; + } + else + { + // Return 404 + status.setCode(404, "Constraint List " + shortName + " does not exist"); + return; + } +} + +main(); \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraint.get.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraint.get.json.ftl new file mode 100644 index 0000000000..d01eb83470 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraint.get.json.ftl @@ -0,0 +1,8 @@ +<#import "rmconstraint.lib.ftl" as rmconstraintLib/> + +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "data": <@rmconstraintLib.constraintJSON constraint=constraint /> + +} + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraint.lib.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraint.lib.ftl new file mode 100644 index 0000000000..f565ec5414 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraint.lib.ftl @@ -0,0 +1,61 @@ +<#-- renders an rm constraint object --> + +<#macro constraintSummaryJSON constraint> +<#escape x as jsonUtils.encodeJSONString(x)> + { + "url" : "${url.serviceContext + "/api/rma/admin/rmconstraints/" + constraint.name}", + "constraintName" : "${constraint.name}", + "constraintTitle" : "${constraint.title}" + } + + + +<#macro constraintJSON constraint> +<#escape x as jsonUtils.encodeJSONString(x)> + { + "url" : "${url.serviceContext + "/api/rma/admin/rmconstraints/" + constraint.name}", + "constraintName" : "${constraint.name}", + "caseSensitive" : "${constraint.caseSensitive?string("true", "false")}", + "constraintTitle" : "${constraint.title}", + "allowedValues" : [ <#list constraint.allowedValues as allowedValue> "${allowedValue}" <#if allowedValue_has_next>, ] + } + + + +<#macro constraintWithValuesJSON constraint> +<#escape x as jsonUtils.encodeJSONString(x)> + { + "url" : "${url.serviceContext + "/api/rma/admin/rmconstraints/" + constraint.name}", + "constraintName" : "${constraint.name}", + "caseSensitive" : "${constraint.caseSensitive?string("true", "false")}", + "constraintTitle" : "${constraint.title}", + "values" : [ + <#list constraint.values as value> + { + "url" : "${url.serviceContext + "/api/rma/admin/rmconstraints/" + constraint.name + "/values/" + value.valueName}", + "valueName":"${value.valueName}", + "valueTitle":"${value.valueTitle}", + "authorities" : [ <#list value.authorities as authority> { "authorityName" : "${authority.authorityName}", "authorityTitle" : "${authority.authorityTitle}"} <#if authority_has_next>,] + }<#if value_has_next>, + + ] + } + + + +<#macro constraintWithValueJSON constraint value> +<#escape x as jsonUtils.encodeJSONString(x)> + { + "url" : "${url.serviceContext + "/api/rma/admin/rmconstraints/" + constraint.name + "/values/" + value.valueName}", + "constraintName" : "${constraint.name}", + "constraintTitle" : "${constraint.title}", + "value" : + { + "url" : "${url.serviceContext + "/api/rma/admin/rmconstraints/" + constraint.name + "/values/" + value.valueName}", + "valueName":"${value.valueName}", + "valueTitle":"${value.valueTitle}", + "authorities" : [ <#list value.authorities as authority> { "authorityName" : "${authority.authorityName}", "authorityTitle" : "${authority.authorityTitle}"} <#if authority_has_next>,] + } + } + + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraint.put.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraint.put.desc.xml new file mode 100644 index 0000000000..b7e4533b8f --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraint.put.desc.xml @@ -0,0 +1,19 @@ + + Update an RM Constraint List + + The following properties may be updatedConstraint object +
+
+
constraintTitle
Optional, the title of the constraint (human readable)
+
allowedValues
Optional, array of allowed values, the complete list must be specified
+
+ ]]> +
+ /api/rma/admin/rmconstraints/{listName} + argument + user + required + internal +
\ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraint.put.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraint.put.json.ftl new file mode 100644 index 0000000000..b572b81729 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraint.put.json.ftl @@ -0,0 +1,7 @@ +<#import "rmconstraint.lib.ftl" as rmconstraintLib/> + +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "data": <@rmconstraintLib.constraintJSON constraint=constraint /> +} + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraint.put.json.js b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraint.put.json.js new file mode 100644 index 0000000000..263e7ef7d8 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraint.put.json.js @@ -0,0 +1,51 @@ +/** + * Update an rm constraint + */ +function main() +{ + // Get the shortname + var shortName = url.extension; + + // Get the constraint + var constraint = caveatConfig.getConstraint(shortName); + + if (constraint != null) + { + var allowedValues + var title = null; + + if (json.has("constraintTitle")) + { + title = json.get("constraintTitle"); + constraint.updateTitle(title); + } + + if (json.has("allowedValues")) + { + values = json.getJSONArray("allowedValues"); + + var i = 0; + allowedValues = new Array(); + + if (values != null) + { + for (var x = 0; x < values.length(); x++) + { + allowedValues[i++] = values.get(x); + } + } + constraint.updateAllowedValues(allowedValues); + } + + // Pass the constraint detail to the template + model.constraint = constraint; + } + else + { + // Return 404 + status.setCode(404, "Constraint List " + shortName + " does not exist"); + return; + } +} + +main(); \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraints.get.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraints.get.desc.xml new file mode 100644 index 0000000000..e8dff744df --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraints.get.desc.xml @@ -0,0 +1,18 @@ + + Get the names of all RM Constraint Lists + +
+
constraintTitle
Human readable title for the custom constraint list
+
constraintName
the name of the constraint list, prefixed
+
url
+
+ ]]> +
+ /api/rma/admin/rmconstraints?withEmptyLists={withEmptyLists?} + argument + user + required + internal +
\ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraints.get.js b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraints.get.js new file mode 100644 index 0000000000..b77cdd6eb1 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraints.get.js @@ -0,0 +1,19 @@ +/** + * List the names of the rm constraints + */ +function main() +{ + var wel = true; + var withEmptyLists = args["withEmptyLists"]; + // Pass the information to the template + if (withEmptyLists != null && withEmptyLists === 'false') + { + model.constraints = caveatConfig.constraintsWithoutEmptyList; + } + else + { + model.constraints = caveatConfig.allConstraints; + } +} + +main(); \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraints.get.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraints.get.json.ftl new file mode 100644 index 0000000000..172e46f633 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraints.get.json.ftl @@ -0,0 +1,13 @@ +<#import "rmconstraint.lib.ftl" as rmconstraintLib/> + +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "data": + [ + <#list constraints as constraint> + <@rmconstraintLib.constraintSummaryJSON constraint=constraint /> + <#if constraint_has_next>, + + ] +} + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraints.post.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraints.post.desc.xml new file mode 100644 index 0000000000..3d0036b1d4 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraints.post.desc.xml @@ -0,0 +1,20 @@ + + Create a new RM Constraint List + + The following properties may be specified +
+
+
constraintName
Optional the name of the constraint. If not specified then one will be generated.
+
constraintTitle
The title of the constraint (human readable)
+
allowedValues
array of allowed values, the complete list must be specified
+
+ ]]> +
+ /api/rma/admin/rmconstraints + argument + user + required + internal +
\ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraints.post.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraints.post.json.ftl new file mode 100644 index 0000000000..b572b81729 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraints.post.json.ftl @@ -0,0 +1,7 @@ +<#import "rmconstraint.lib.ftl" as rmconstraintLib/> + +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "data": <@rmconstraintLib.constraintJSON constraint=constraint /> +} + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraints.post.json.js b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraints.post.json.js new file mode 100644 index 0000000000..4c8b5a0804 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/rmconstraints.post.json.js @@ -0,0 +1,47 @@ +/** + * Create a new RM Constraint List + */ +function main() +{ + // Parse the passed in details + var title = null; + var name = null; + var allowedValues = {}; + + if (json.has("allowedValues")) + { + values = json.getJSONArray("allowedValues"); + + var i = 0; + allowedValues = new Array(); + + if (values != null) + { + for (var x = 0; x < values.length(); x++) + { + allowedValues[i++] = values.get(x); + } + } + } + + if (json.has("constraintName")) + { + name = json.get("constraintName"); + } + + if (json.has("constraintTitle")) + { + title = json.get("constraintTitle"); + } + else + { + title = name; + } + + var constraint = caveatConfig.createConstraint(name, title, allowedValues); + + // Pass the constraint detail to the template + model.constraint = constraint; +} + +main(); \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraint.get.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraint.get.desc.xml new file mode 100644 index 0000000000..e9f2517b1c --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraint.get.desc.xml @@ -0,0 +1,63 @@ + + Get an RM Constraint + + + The list name is qualified with an underscore between prefix and name to be compatible with Java Script and URLs +
+ e.g. rma_listName rather than rma:listName +
+ Constraint object +
+
constraintName
the name of the constraint. The underscore character is used instead of the colon
+
caseSensitive
is the constraint case sensitive
+
constraintTitle
the display name of the constraint
+
allowedValues
array of the allowed values, this is the complete unrestricted list of all values
+
values
array of constraint values
+
+ Constraint values object +
+
valueName
the full name of the value
+
valueTitle
the display name of the value
+
authorities
array of constraint authorities
+
+ Example JSON data +
+   {
+      "data":
+      {
+         "url" : "\/alfresco\/service\/api\/rma\/admin\/rmconstraints\/rma_smList",
+         "constraintName" : "rma_smList",
+         "caseSensitive" :  "true",
+         "constraintTitle" : "Display title for rma:smList",
+         "allowedValues" : [ "Alpha" ,  "Beta" ,  "Gamma" ],
+         "values" :
+         [
+            {
+               "valueName":"NOCON",
+               "valueTitle":"NOCON",
+               "authorities" : [  { "authorityName" : "jrogers", "authorityTitle" : "jrogers"} ]
+            },
+            {
+               "valueName":"NOFORN",
+               "valueTitle":"NOFORN",
+               "authorities" : [  { "authorityName" : "jrogers", "authorityTitle" : "jrogers"} , { "authorityName" : "fbloggs", "authorityTitle" : "fbloggs"} , { "authorityName" : "jdoe", "authorityTitle" : "jdoe"} ]
+            },
+            {
+               "valueName":"FGI",
+               "valueTitle":"FGI",
+               "authorities" : [  { "authorityName" : "fbloggs", "authorityTitle" : "fbloggs"} , { "authorityName" : "jdoe", "authorityTitle" : "jdoe"} ]
+            }
+         ]
+      }
+   }
+   
+ ]]> +
+ /api/rma/admin/rmconstraints/{listName}/values + argument + user + required + internal +
\ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraint.get.js b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraint.get.js new file mode 100644 index 0000000000..74261a660e --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraint.get.js @@ -0,0 +1,25 @@ +/** + * Get the detail of the rm constraint + */ +function main() +{ + var urlElements = url.extension.split("/"); + var shortName = urlElements[0]; + + // Get the constraint + var constraint = caveatConfig.getConstraint(shortName); + + if (constraint != null) + { + // Pass the constraint detail to the template + model.constraint = constraint; + } + else + { + // Return 404 + status.setCode(404, "Constraint List " + shortName + " does not exist"); + return; + } +} + +main(); \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraint.get.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraint.get.json.ftl new file mode 100644 index 0000000000..ba9688a12b --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraint.get.json.ftl @@ -0,0 +1,7 @@ +<#import "../rmconstraint.lib.ftl" as rmconstraintLib/> + +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "data": <@rmconstraintLib.constraintWithValuesJSON constraint=constraint /> +} + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraint.post.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraint.post.desc.xml new file mode 100644 index 0000000000..d6c4af865f --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraint.post.desc.xml @@ -0,0 +1,28 @@ + + Updates values in an an RM Constraint List + + If the authority is missing from the list then the value is deleted. +
+ If an authority does not exist in a list then the authority is added. +
+ Only the authorities for the specified values are updated. +
+ If a value is missing it will not be affected. +
+ JSON Parameter format: +
+ "values" : [ ValueName, [ authorityName1, authorityName2 ]] +
+ The input format for values is different to the output format. +
+ Data Return format. + ]]> +
+ /api/rma/admin/rmconstraints/{listName}/values + argument + user + required + internal +
\ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraint.post.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraint.post.json.ftl new file mode 100644 index 0000000000..ba9688a12b --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraint.post.json.ftl @@ -0,0 +1,7 @@ +<#import "../rmconstraint.lib.ftl" as rmconstraintLib/> + +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "data": <@rmconstraintLib.constraintWithValuesJSON constraint=constraint /> +} + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraint.post.json.js b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraint.post.json.js new file mode 100644 index 0000000000..731432cc78 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraint.post.json.js @@ -0,0 +1,39 @@ +/** + * Update the details of a value in an rm constraint + */ +function main() +{ + var urlElements = url.extension.split("/"); + var shortName = urlElements[0]; + + var values = null; + + if (json.has("values")) + { + values = json.getJSONArray("values"); + } + + if (values == null) + { + status.setCode(status.STATUS_BAD_REQUEST, "Values missing"); + return; + } + + // Get the constraint + var constraint = caveatConfig.getConstraint(shortName); + + if (constraint != null) + { + constraint.updateValues(values); + model.constraint = caveatConfig.getConstraint(shortName); + model.constraintName = shortName; + } + else + { + // Return 404 + status.setCode(404, "Constraint List " + shortName + " does not exist"); + return; + } +} + +main(); \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraintvalue.delete.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraintvalue.delete.desc.xml new file mode 100644 index 0000000000..74b3f1330e --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraintvalue.delete.desc.xml @@ -0,0 +1,11 @@ + + Delete a value from an RM Constraint List + + + /api/rma/admin/rmconstraints/{listName}/{valueName} + argument + user + required + internal + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraintvalue.delete.js b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraintvalue.delete.js new file mode 100644 index 0000000000..28d5e19382 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraintvalue.delete.js @@ -0,0 +1,41 @@ +/** + * Delete the rm constraint list + */ +function main() +{ + var urlElements = url.extension.split("/"); + var shortName = urlElements[0]; + var authorityName = urlElements[1]; + + if (shortName == null) + { + status.setCode(status.STATUS_BAD_REQUEST, "shortName missing"); + return; + } + if (valueName == null) + { + status.setCode(status.STATUS_BAD_REQUEST, "value missing"); + return; + } + + // Get the constraint + var constraint = caveatConfig.getConstraint(shortName); + + if (constraint != null) + { + caveatConfig.deleteRMConstraintListValue(shortName, valueName); + + var constraint = caveatConfig.getConstraint(shortName); + + // Pass the constraint name to the template + model.constraint = constraint; + } + else + { + // Return 404 + status.setCode(404, "Constraint List " + shortName + " does not exist"); + return; + } +} + +main(); \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraintvalue.delete.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraintvalue.delete.json.ftl new file mode 100644 index 0000000000..c811deeed9 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraintvalue.delete.json.ftl @@ -0,0 +1,7 @@ +<#import "../rmconstraint.lib.ftl" as rmconstraintLib/> + +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "data": <@rmconstraintLib.constraintWithValuesJSON constraint=constraint /> +} + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraintvalue.get.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraintvalue.get.desc.xml new file mode 100644 index 0000000000..482c2ebf21 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraintvalue.get.desc.xml @@ -0,0 +1,47 @@ + + Get an RM Constraint Value + + + The list name is qualified with an underscore between prefix and name to be compatible with Java Script and URLs +
+ e.g. rma_listName rather than rma:listName +
+ Constraint object +
+
constraintName
the name of the constraint. The underscore character is used instead of the colon
+
constraintTitle
the display name of the constraint
+
value
constraint values
+
+ Constraint values object +
+
valueName
the full name of the value
+
valueTitle
the display name of the value
+
authorities
array of constraint authorities
+
+ Example JSON data: +
+   {
+      "data":
+      {
+         "url" : "\/alfresco\/service\/api\/rma\/admin\/rmconstraints\/rma_smList/values/NOCON",
+         "constraintName" : "rma_smList",
+         "constraintTitle" : "Display title for rma:smList",
+         "value" :
+         {
+           "valueName":"NOCON", 
+           "valueTitle":"NOCON",
+           "authorities" : [  { "authorityName" : "jrogers", "authorityTitle" : "jrogers"} ]
+         }
+      }
+   }
+   
+ ]]> +
+ /api/rma/admin/rmconstraints/{listName}/values/{valueName} + argument + user + required + internal +
\ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraintvalue.get.js b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraintvalue.get.js new file mode 100644 index 0000000000..5bf8b82f17 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraintvalue.get.js @@ -0,0 +1,36 @@ +/** + * Get the detail of the rm constraint + */ +function main() +{ + var urlElements = url.extension.split("/"); + var shortName = decodeURIComponent(urlElements[0]); + var valueName = decodeURIComponent(urlElements[2]) + + // Get the constraint + var constraint = caveatConfig.getConstraint(shortName); + + if (constraint != null) + { + // Pass the constraint detail to the template + var value = constraint.getValue(valueName); + + if(value == null) + { + // Return 404 + status.setCode(404, "Constraint List: " + shortName + " value: " + valueName + "does not exist"); + return; + } + + model.value = value; + model.constraint = constraint; + } + else + { + // Return 404 + status.setCode(404, "Constraint List " + shortName + " does not exist"); + return; + } +} + +main(); \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraintvalue.get.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraintvalue.get.json.ftl new file mode 100644 index 0000000000..392a3b2ac0 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmconstraint/values/rmconstraintvalue.get.json.ftl @@ -0,0 +1,7 @@ +<#import "../rmconstraint.lib.ftl" as rmconstraintLib/> + +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "data": <@rmconstraintLib.constraintWithValueJSON constraint=constraint value=value/> +} + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevent.delete.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevent.delete.desc.xml new file mode 100644 index 0000000000..0c4801c54a --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevent.delete.desc.xml @@ -0,0 +1,14 @@ + + Deletes a records management event + + + + /api/rma/admin/rmevents/{eventname} + argument + user + required + internal + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevent.delete.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevent.delete.json.ftl new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevent.delete.json.ftl @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevent.get.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevent.get.desc.xml new file mode 100644 index 0000000000..e7e1294a75 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevent.get.desc.xml @@ -0,0 +1,14 @@ + + Get an records management event + + + + /api/rma/admin/rmevents/{eventname} + argument + user + required + internal + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevent.get.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevent.get.json.ftl new file mode 100644 index 0000000000..7d43337683 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevent.get.json.ftl @@ -0,0 +1,8 @@ +<#import "rmevent.lib.ftl" as rmEventLib/> + +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "data": + <@rmEventLib.eventJSON event=event /> +} + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevent.lib.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevent.lib.ftl new file mode 100644 index 0000000000..53d3e1fff8 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevent.lib.ftl @@ -0,0 +1,12 @@ +<#-- renders an rm event object --> + +<#macro eventJSON event> +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "eventName": "${event.name}", + "eventDisplayLabel": "${event.displayLabel}", + "eventType":"${event.type}" +} + + + diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevent.put.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevent.put.desc.xml new file mode 100644 index 0000000000..3457e2ee65 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevent.put.desc.xml @@ -0,0 +1,14 @@ + + Update a records management event + + + + /api/rma/admin/rmevents/{eventname} + argument + user + required + internal + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevent.put.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevent.put.json.ftl new file mode 100644 index 0000000000..7d43337683 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevent.put.json.ftl @@ -0,0 +1,8 @@ +<#import "rmevent.lib.ftl" as rmEventLib/> + +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "data": + <@rmEventLib.eventJSON event=event /> +} + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevents.get.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevents.get.desc.xml new file mode 100644 index 0000000000..ca2c548783 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevents.get.desc.xml @@ -0,0 +1,14 @@ + + Get list of records management events + + + + /api/rma/admin/rmevents + argument + user + required + internal + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevents.get.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevents.get.json.ftl new file mode 100644 index 0000000000..ebe9d531a6 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevents.get.json.ftl @@ -0,0 +1,13 @@ +<#import "rmevent.lib.ftl" as rmEventLib/> + +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "data": + { + <#list events as event> + "${event.name}": + <@rmEventLib.eventJSON event=event /><#if event_has_next>, + + } +} + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevents.post.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevents.post.desc.xml new file mode 100644 index 0000000000..ed0a8207d5 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevents.post.desc.xml @@ -0,0 +1,14 @@ + + Create a new records managment event + + + + /api/rma/admin/rmevents + argument + user + required + internal + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevents.post.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevents.post.json.ftl new file mode 100644 index 0000000000..8a9faca5d6 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmevents.post.json.ftl @@ -0,0 +1,9 @@ +<#import "rmevent.lib.ftl" as rmEventLib/> + +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "data": + <@rmEventLib.eventJSON event=event /> + } +} + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmeventtypes.get.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmeventtypes.get.desc.xml new file mode 100644 index 0000000000..d14aecb466 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmeventtypes.get.desc.xml @@ -0,0 +1,14 @@ + + Gets the records management event types + + + + /api/rma/admin/rmeventtypes + argument + user + required + internal + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmeventtypes.get.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmeventtypes.get.json.ftl new file mode 100644 index 0000000000..21d6c3a7e3 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmevent/rmeventtypes.get.json.ftl @@ -0,0 +1,16 @@ +<#import "rmevent.lib.ftl" as rmEventLib/> + +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "data": + { + <#list eventtypes as eventtype> + "${eventtype.name}": + { + "eventTypeName" : "${eventtype.name}", + "eventTypeDisplayLabel" : "${eventtype.displayLabel}" + }<#if eventtype_has_next>, + + } +} + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmrole.delete.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmrole.delete.desc.xml new file mode 100644 index 0000000000..c7b6c75835 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmrole.delete.desc.xml @@ -0,0 +1,14 @@ + + Deletes a records management role + + + + /api/rma/admin/rmroles/{rolename} + argument + user + required + internal + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmrole.delete.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmrole.delete.json.ftl new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmrole.delete.json.ftl @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmrole.get.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmrole.get.desc.xml new file mode 100644 index 0000000000..21693f3819 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmrole.get.desc.xml @@ -0,0 +1,14 @@ + + Get an records management role + + + + /api/rma/admin/rmroles/{rolename} + argument + user + required + internal + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmrole.get.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmrole.get.json.ftl new file mode 100644 index 0000000000..3a4a96b70f --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmrole.get.json.ftl @@ -0,0 +1,8 @@ +<#import "rmrole.lib.ftl" as rmRoleLib/> + +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "data": + <@rmRoleLib.roleJSON role=role /> +} + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmrole.lib.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmrole.lib.ftl new file mode 100644 index 0000000000..48b4c854d8 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmrole.lib.ftl @@ -0,0 +1,15 @@ +<#-- renders an rm role object --> +<#macro roleJSON role> +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "name": "${role.name}", + "displayLabel": "${role.displayLabel}", + "capabilities" : + [ + <#list role.capabilities as capability> + "${capability}"<#if capability_has_next>, + + ] +} + + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmrole.put.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmrole.put.desc.xml new file mode 100644 index 0000000000..7849806794 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmrole.put.desc.xml @@ -0,0 +1,14 @@ + + Update a records management role + + + + /api/rma/admin/rmroles/{rolename} + argument + user + required + internal + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmrole.put.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmrole.put.json.ftl new file mode 100644 index 0000000000..3a4a96b70f --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmrole.put.json.ftl @@ -0,0 +1,8 @@ +<#import "rmrole.lib.ftl" as rmRoleLib/> + +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "data": + <@rmRoleLib.roleJSON role=role /> +} + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmroles.get.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmroles.get.desc.xml new file mode 100644 index 0000000000..c3dd070b97 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmroles.get.desc.xml @@ -0,0 +1,14 @@ + + Get list of records management roles + + + + /api/rma/admin/rmroles?user={user?} + argument + user + required + internal + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmroles.get.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmroles.get.json.ftl new file mode 100644 index 0000000000..5cf2281975 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmroles.get.json.ftl @@ -0,0 +1,13 @@ +<#import "rmrole.lib.ftl" as rmRoleLib/> + +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "data": + { + <#list roles as role> + "${role.name}": + <@rmRoleLib.roleJSON role=role /><#if role_has_next>, + + } +} + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmroles.post.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmroles.post.desc.xml new file mode 100644 index 0000000000..205815899c --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmroles.post.desc.xml @@ -0,0 +1,14 @@ + + Create a new records managment role + + + + /api/rma/admin/rmroles + argument + user + required + internal + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmroles.post.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmroles.post.json.ftl new file mode 100644 index 0000000000..3a4a96b70f --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/admin/rmrole/rmroles.post.json.ftl @@ -0,0 +1,8 @@ +<#import "rmrole.lib.ftl" as rmRoleLib/> + +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "data": + <@rmRoleLib.roleJSON role=role /> +} + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/applydodcertmodelfixes.get.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/applydodcertmodelfixes.get.desc.xml new file mode 100644 index 0000000000..bcc2921b4e --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/applydodcertmodelfixes.get.desc.xml @@ -0,0 +1,15 @@ + + Applies fixes to the content model for DoD certification. + + Please note that this script will be removed after the certification process is complete. + ]]> + /api/rma/applydodcertmodelfixes + argument + admin + required + internal + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/applydodcertmodelfixes.get.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/applydodcertmodelfixes.get.json.ftl new file mode 100644 index 0000000000..53ac8f9883 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/applydodcertmodelfixes.get.json.ftl @@ -0,0 +1,5 @@ +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "success": ${success?string} +} + diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/applyfixmob1573.get.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/applyfixmob1573.get.desc.xml new file mode 100644 index 0000000000..9dada0f840 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/applyfixmob1573.get.desc.xml @@ -0,0 +1,9 @@ + + Applies fix for MOB-1573. + Fixes the RM custom model by changing the multiplicity on custom references to many-to-many. + /api/rma/applyfixmob1573 + argument + admin + required + internal + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/applyfixmob1573.get.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/applyfixmob1573.get.json.ftl new file mode 100644 index 0000000000..53ac8f9883 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/applyfixmob1573.get.json.ftl @@ -0,0 +1,5 @@ +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "success": ${success?string} +} + diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/bootstraptestdata.get.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/bootstraptestdata.get.desc.xml new file mode 100644 index 0000000000..18aa65c8ba --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/bootstraptestdata.get.desc.xml @@ -0,0 +1,9 @@ + + Load the RM DOD bootstrap data. + WebScript to import and fix up the RM DOD bootstrap data. + /api/rma/bootstraptestdata?site={site?}&import={import?} + argument + admin + required + internal + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/bootstraptestdata.get.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/bootstraptestdata.get.json.ftl new file mode 100644 index 0000000000..53ac8f9883 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/bootstraptestdata.get.json.ftl @@ -0,0 +1,5 @@ +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "success": ${success?string} +} + diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customisable.get.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customisable.get.desc.xml new file mode 100644 index 0000000000..384249493f --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customisable.get.desc.xml @@ -0,0 +1,12 @@ + + Records Management Customisable Types and Aspects + + + /api/rma/admin/customisable + + user + required + internal + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customisable.get.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customisable.get.json.ftl new file mode 100644 index 0000000000..6009a0eb91 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customisable.get.json.ftl @@ -0,0 +1,14 @@ +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "data": + [ + <#list items as item> + { + "name" : "${item.name}", + "isAspect" : ${item.isAspect?string}, + "title" : "${item.title}" + }<#if item_has_next>, + + ] +} + diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/custompropdefinition.delete.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/custompropdefinition.delete.desc.xml new file mode 100644 index 0000000000..90aa0b9af9 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/custompropdefinition.delete.desc.xml @@ -0,0 +1,18 @@ + + Attempts to remove specified Custom Property Definitions from the custom model + + It should be noted that it may not be possible to honour this request in all cases.
+ In cases where instances of the specified property are already present in the system,
+ the request will not succeed. In cases where there are no instances of the specified
+ property in the system, the request will attempted.
+
+ The propId is that returned by custompropertydefinitions.get. + ]]> +
+ /api/rma/admin/custompropertydefinitions/{propId} + argument + user + required + internal +
diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/custompropdefinition.delete.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/custompropdefinition.delete.json.ftl new file mode 100644 index 0000000000..f23634ccec --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/custompropdefinition.delete.json.ftl @@ -0,0 +1,8 @@ +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "data": + { + "customProperty": "${propertyqname}" + } +} + diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/custompropdefinition.post.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/custompropdefinition.post.desc.xml new file mode 100644 index 0000000000..9d5af56a68 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/custompropdefinition.post.desc.xml @@ -0,0 +1,29 @@ + + Add a Custom Property Definition to the custom model + +
+ The URL query parameter 'element' defines which RM type will be able to have the property added.
+ It should be a the customisable types short qname type (eg rma:recordCategory)
+
+ The JSON parameter 'propId' is optional. If a value is provided it must only contain characters
+ which are legal within URLs and within QNames.
+ It is also the responsibility of the calling code to ensure the propId is unique across all custom properties.
+ If a value is not provided, one will be generated.
+
+ The body of the post should be in the form, e.g.
+ {
+    "label": "sample Custom Property",
+    "dataType": "d:boolean",
+    "mandatory": false
, +    "constraintRef": "rmc:constraintName",
+    "propId": "myPropId"
+ }
+ ]]> +
+ /api/rma/admin/custompropertydefinitions?element={element} + argument + user + required + internal +
diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/custompropdefinition.post.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/custompropdefinition.post.json.ftl new file mode 100644 index 0000000000..ef996b6013 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/custompropdefinition.post.json.ftl @@ -0,0 +1,6 @@ +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "propId": "${propId}", + "url": "${url}" +} + diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/custompropdefinition.put.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/custompropdefinition.put.desc.xml new file mode 100644 index 0000000000..f4dcf82e60 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/custompropdefinition.put.desc.xml @@ -0,0 +1,25 @@ + + Updates a Custom Property Definition. + +
+ There is currently support only for updating the label and/or for updating the constraint.
+ The body of the PUT should be in the form, e.g.
+ {
+    "label": "updated label value",
+    "constraintRef": "rmc:constraintName",
+ }
+ In the above example JSON, a constraintRef with QName "rmc:constraintName" will be added to the
+ property definition if one does not exist. If there already is a constraint, it will be replaced.
+ It is also possible to remove all constraints from the property definition by passing null:
+ {
+    "constraintRef": null,
+ }
+ ]]> +
+ /api/rma/admin/custompropertydefinitions/{propId} + argument + user + required + internal +
diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/custompropdefinition.put.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/custompropdefinition.put.json.ftl new file mode 100644 index 0000000000..ef996b6013 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/custompropdefinition.put.json.ftl @@ -0,0 +1,6 @@ +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "propId": "${propId}", + "url": "${url}" +} + diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/custompropdefinitions.get.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/custompropdefinitions.get.desc.xml new file mode 100644 index 0000000000..bd6b7b218c --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/custompropdefinitions.get.desc.xml @@ -0,0 +1,14 @@ + + Records Management Custom Model Property Definitions + + If a propId is specified within the URL, only that specific property definition is returned.
+ ]]> +
+ /api/rma/admin/custompropertydefinitions?element={element} + /api/rma/admin/custompropertydefinitions/{propId} + + user + required + internal +
\ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/custompropdefinitions.get.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/custompropdefinitions.get.json.ftl new file mode 100644 index 0000000000..bbdee48371 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/custompropdefinitions.get.json.ftl @@ -0,0 +1,43 @@ +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "data": + { + "customProperties": + { + <#list customProps as prop> + "${prop.name.toPrefixString()}": + { + "dataType": "<#if prop.dataType??>${prop.dataType.name.toPrefixString()}", + "label": "${prop.title!""}", + "description": "${prop.description!""}", + "mandatory": ${prop.mandatory?string}, + "multiValued": ${prop.multiValued?string}, + "defaultValue": "${prop.defaultValue!""}", + "protected": ${prop.protected?string}, + "propId": "${prop.name.localName}", + "constraintRefs": + [ + <#list prop.constraints as con> + { + "name": "${con.constraint.shortName!""}", + "title": "${con.title!""}", + "type": "${con.constraint.type!""}", + "parameters": + { + <#-- Basic implementation. Only providing 2 hardcoded parameters. --> + <#assign lov = con.constraint.parameters["allowedValues"]> + "caseSensitive": ${con.constraint.parameters["caseSensitive"]?string}, + "listOfValues" : + [ + <#list lov as val>"${val}"<#if val_has_next>, + ] + } + }<#if con_has_next>, + + ] + }<#if prop_has_next>, + + } + } +} + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customref.delete.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customref.delete.desc.xml new file mode 100644 index 0000000000..464533568d --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customref.delete.desc.xml @@ -0,0 +1,14 @@ + + Removes specified Custom Reference Instance from between the specified nodes + + The nodeRef encoded within the URL path is the 'fromNode' in the relationship.
+ The nodeRef encoded within the query string is the 'toNode' in the relationship.
+ ]]> +
+ /api/node/{store_type}/{store_id}/{id}/customreferences/{refId}?st={toStoreType}&si={toStoreId}&id={toId} + argument + user + required + internal +
diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customref.delete.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customref.delete.json.ftl new file mode 100644 index 0000000000..f539b37200 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customref.delete.json.ftl @@ -0,0 +1,3 @@ +{ + "success": ${success?string} +} \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customref.post.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customref.post.desc.xml new file mode 100644 index 0000000000..660c6f63c7 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customref.post.desc.xml @@ -0,0 +1,18 @@ + + Add a Custom Reference instance to the specified record node + +
+ The body of the post should be in the form, e.g.
+ {
+    "toNode" : "workspace://SpacesStore/12345678-abcd-1234-abcd-1234567890ab",
+    "refId" : the refId as returned by customrefdefinitions.get
+ }
+ ]]> +
+ /api/node/{store_type}/{store_id}/{id}/customreferences + argument + user + required + internal +
\ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customref.post.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customref.post.json.ftl new file mode 100644 index 0000000000..1ef9fab7d9 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customref.post.json.ftl @@ -0,0 +1,5 @@ +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "success": ${success?string} +} + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customrefdefinition.post.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customrefdefinition.post.desc.xml new file mode 100644 index 0000000000..d5a793f9ef --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customrefdefinition.post.desc.xml @@ -0,0 +1,22 @@ + + Add a Custom Reference Definition to the custom model + +
+ The body of the post should be in the form, e.g.
+ {
+    "referenceType" : ""parentchild" OR "bidirectional",
+    "label" : "bar"
+    "source" : "foo",
+    "target" : "bar"
+ }
+ For parentchild references, source and target must be provided. For bidirectional references, + a label is required.
+ ]]> +
+ /api/rma/admin/customreferencedefinitions + argument + user + required + internal +
diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customrefdefinition.post.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customrefdefinition.post.json.ftl new file mode 100644 index 0000000000..1216a4579d --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customrefdefinition.post.json.ftl @@ -0,0 +1,10 @@ +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "success": ${success?string}, + "data" : { + "referenceType": "${referenceType?string}", + "refId": "${refId?string}", + "url": "${url?string}" + } +} + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customrefdefinition.put.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customrefdefinition.put.desc.xml new file mode 100644 index 0000000000..45cf4a2301 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customrefdefinition.put.desc.xml @@ -0,0 +1,25 @@ + + Updates a Custom Reference Definition. + +
+ There is currently support only for updating the label, source or target fields.
+
+ The body of the PUT should be in the form, e.g.
+ {
+    "label": "updated label value",
+ }
+ OR + {
+    "source": "updated source value",
+    "target": "updated target value",
+ }
+ for bidirectional and parentchild references respectively.
+ ]]> +
+ /api/rma/admin/customreferencedefinitions/{refId} + argument + user + required + internal +
diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customrefdefinition.put.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customrefdefinition.put.json.ftl new file mode 100644 index 0000000000..ae7ddd675b --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customrefdefinition.put.json.ftl @@ -0,0 +1,6 @@ +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "refId": "${refId}", + "url": "${url}" +} + diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customrefdefinitions.get.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customrefdefinitions.get.desc.xml new file mode 100644 index 0000000000..b474afd1ee --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customrefdefinitions.get.desc.xml @@ -0,0 +1,15 @@ + + Records Management Custom Model Reference Definitions + + If a refId is specified, then only the reference definition corresponding to that
+ id will be returned.
+ ]]> +
+ /api/rma/admin/customreferencedefinitions + /api/rma/admin/customreferencedefinitions/{refId} + + user + required + internal +
\ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customrefdefinitions.get.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customrefdefinitions.get.json.ftl new file mode 100644 index 0000000000..d4d7fa9331 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customrefdefinitions.get.json.ftl @@ -0,0 +1,16 @@ +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "data": + { + "customReferences": + [ + <#list customRefs as ref> + { + <#assign keys = ref?keys> + <#list keys as key>"${key}": "${ref[key]}"<#if key_has_next>, + }<#if ref_has_next>, + + ] + } +} + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customrefs.get.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customrefs.get.desc.xml new file mode 100644 index 0000000000..ff5c9a054b --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customrefs.get.desc.xml @@ -0,0 +1,45 @@ + + Records Management Custom Reference Instances + + The response will have the form:
+ {
+ "data":
+   {
+   "nodeName": "samplename",
+   "nodeTitle": "sample title",
+   "customReferencesFrom":
+     [
+       {
+       "refId": "09876543-wxyz-0987-wxyz-098765432109",
+       "referenceType": "bidirectional",
+       "label": "BiDi",
+       "targetRef": "workspace://SpacesStore/zyxwvuts-4321-zyxw-4321-zyxwvutsrqpo",
+       "sourceRef": "workspace://SpacesStore/a1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1"
+       }
+     ]
+   "customReferencesTo":
+     [
+       {
+       "childRef": "workspace://SpacesStore/12345678-abcd-1234-abcd-123456789012",
+       "refId": "versions",
+       "source": "VersionedBy",
+       "referenceType": "parentchild",
+       "target": "Versions",
+       "parentRef": "workspace://SpacesStore/abcdefgh-1234-abcd-1234-abcdefghijkl"
+       }
+     ]
+   }
+ }
+ The "customReferencesFrom" field gives the references that are from this node i.e. from the node
+ to which the GET was issued. Conversely, the "customReferencesTo" field gives the references that
+ are to this node.
+ For parent/child reference types, the reference goes from the parent to the child.
+ ]]> +
+ /api/node/{store_type}/{store_id}/{id}/customreferences + + user + required + internal +
\ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customrefs.get.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customrefs.get.json.ftl new file mode 100644 index 0000000000..8f38b39d90 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/customrefs.get.json.ftl @@ -0,0 +1,27 @@ +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "data": + { + "nodeName": "${nodeName!""}", + "nodeTitle": "${nodeTitle!""}", + "customReferencesFrom": + [ + <#list customRefsFrom as ref> + { + <#assign keys = ref?keys> + <#list keys as key>"${key}": "${ref[key]}"<#if key_has_next>, + }<#if ref_has_next>, + + ], + "customReferencesTo": + [ + <#list customRefsTo as ref> + { + <#assign keys = ref?keys> + <#list keys as key>"${key}": "${ref[key]}"<#if key_has_next>, + }<#if ref_has_next>, + + ] + } +} + diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionactiondefinition.delete.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionactiondefinition.delete.desc.xml new file mode 100644 index 0000000000..cd9df1aa22 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionactiondefinition.delete.desc.xml @@ -0,0 +1,9 @@ + + Delete Disposition Action Definition + Deletes a disposition action definition from the collection. + /api/node/{store_type}/{store_id}/{id}/dispositionschedule/dispositionactiondefinitions/{action_def_id} + argument + user + required + internal + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionactiondefinition.delete.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionactiondefinition.delete.json.ftl new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionactiondefinition.delete.json.ftl @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionactiondefinition.lib.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionactiondefinition.lib.ftl new file mode 100644 index 0000000000..33df310043 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionactiondefinition.lib.ftl @@ -0,0 +1,17 @@ +<#macro actionJSON action> +<#escape x as jsonUtils.encodeJSONString(x)> + { + "id": "${action.id}", + "url": "${action.url}", + "index": ${action.index}, + "name": "${action.name}", + "label": "${action.label}", + <#if action.description??>"description": "${action.description}", + <#if action.period??>"period": "${action.period}", + <#if action.periodProperty??>"periodProperty": "${action.periodProperty}", + <#if action.location??>"location": "${action.location}", + <#if action.events??>"events": [<#list action.events as event>"${event}"<#if event_has_next>,], + "eligibleOnFirstCompleteEvent": ${action.eligibleOnFirstCompleteEvent?string} + } + + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionactiondefinition.put.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionactiondefinition.put.desc.xml new file mode 100644 index 0000000000..66692a049a --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionactiondefinition.put.desc.xml @@ -0,0 +1,21 @@ + + Update Disposition Action Definition + + {
+    "name" : Name of action,
+    "description" : Description of the action definition,
+    "period" : The period of time,
+    "periodProperty" : Model property the period is relative to,
+    "eligibleOnFirstCompleteEvent" : Whether all events have to occur,
+    "events" : [List of event names]
+ }
+ ]]> +
+ /api/node/{store_type}/{store_id}/{id}/dispositionschedule/dispositionactiondefinitions/{action_def_id} + argument + user + required + internal +
\ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionactiondefinition.put.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionactiondefinition.put.json.ftl new file mode 100644 index 0000000000..c9a440057d --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionactiondefinition.put.json.ftl @@ -0,0 +1,5 @@ +<#import "dispositionactiondefinition.lib.ftl" as actionDefLib/> +{ + "data": + <@actionDefLib.actionJSON action=action/> +} \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionactiondefinitions.post.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionactiondefinitions.post.desc.xml new file mode 100644 index 0000000000..a2e5880649 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionactiondefinitions.post.desc.xml @@ -0,0 +1,21 @@ + + Add Disposition Action Definition + + {
+    "name" : Name of action,
+    "description" : Description of the action definition,
+    "period" : The period of time,
+    "periodProperty" : Model property the period is relative to,
+    "eligibleOnFirstCompleteEvent" : Whether all events have to occur,
+    "events" : [List of event names]
+ }
+ ]]> +
+ /api/node/{store_type}/{store_id}/{id}/dispositionschedule/dispositionactiondefinitions + argument + user + required + internal +
\ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionactiondefinitions.post.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionactiondefinitions.post.json.ftl new file mode 100644 index 0000000000..0a0aa2d064 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionactiondefinitions.post.json.ftl @@ -0,0 +1,5 @@ +<#import "dispositionactiondefinition.lib.ftl" as actionDefLib/> +{ + "data": + <@actionDefLib.actionJSON action=action/> +} diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionlifecycle.get.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionlifecycle.get.desc.xml new file mode 100644 index 0000000000..f80ee5b24b --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionlifecycle.get.desc.xml @@ -0,0 +1,9 @@ + + Get Dispositon Lifecycle + Returns Disposition Lifecycle data + /api/node/{store_type}/{store_id}/{id}/nextdispositionaction + argument + user + required + internal + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionlifecycle.get.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionlifecycle.get.json.ftl new file mode 100644 index 0000000000..4836277100 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionlifecycle.get.json.ftl @@ -0,0 +1,35 @@ +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "data": + { + "url": "${nextaction.url}", + "name": "${nextaction.name}", + "label": "${nextaction.label}", + "eventsEligible": ${nextaction.eventsEligible?string}, + <#if nextaction.asOf??>"asOf": "${nextaction.asOf}", + <#if nextaction.startedAt??>"startedAt": "${nextaction.startedAt}", + <#if nextaction.startedBy??>"startedBy": "${nextaction.startedBy}", + <#if nextaction.startedByFirstName??>"startedByFirstName": "${nextaction.startedByFirstName}", + <#if nextaction.startedByLastName??>"startedByLastName": "${nextaction.startedByLastName}", + <#if nextaction.completedAt??>"completedAt": "${nextaction.completedAt}", + <#if nextaction.completedBy??>"completedBy": "${nextaction.completedBy}", + <#if nextaction.completedByFirstName??>"completedByFirstName": "${nextaction.completedByFirstName}", + <#if nextaction.completedByLastName??>"completedByLastName": "${nextaction.completedByLastName}", + "events": + [ + <#list nextaction.events as event> + { + "name": "${event.name}", + "label": "${event.label}", + "complete": ${event.complete?string}, + <#if event.completedAt??>"completedAt": "${event.completedAt}", + <#if event.completedBy??>"completedBy": "${event.completedBy}", + <#if event.completedByFirstName??>"completedByFirstName": "${event.completedByFirstName}", + <#if event.completedByLastName??>"completedByLastName": "${event.completedByLastName}", + "automatic": ${event.automatic?string} + }<#if event_has_next>, + + ] + } +} + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionschedule.get.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionschedule.get.desc.xml new file mode 100644 index 0000000000..cc21bc2ec4 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionschedule.get.desc.xml @@ -0,0 +1,9 @@ + + Get Dispositon Schedule + Returns Disposition Schedule + /api/node/{store_type}/{store_id}/{id}/dispositionschedule?inherited={inherited?} + argument + user + required + internal + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionschedule.get.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionschedule.get.json.ftl new file mode 100644 index 0000000000..251d15855c --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dispositionschedule.get.json.ftl @@ -0,0 +1,29 @@ +<#import "dispositionactiondefinition.lib.ftl" as actionDefLib/> + +<@scheduleJSON schedule=schedule/> + +<#macro scheduleJSON schedule> +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "data": + { + "url": "${schedule.url}", + "nodeRef": "${schedule.nodeRef}", + <#if schedule.authority??>"authority": "${schedule.authority}", + <#if schedule.instructions??>"instructions": "${schedule.instructions}", + "unpublishedUpdates" : ${schedule.unpublishedUpdates?string}, + "publishInProgress" : ${schedule.publishInProgress?string}, + "recordLevelDisposition": ${schedule.recordLevelDisposition?string}, + "canStepsBeRemoved": ${schedule.canStepsBeRemoved?string}, + "actionsUrl": "${schedule.actionsUrl}", + "actions": + [ + <#list schedule.actions as action> + <@actionDefLib.actionJSON action=action/> + <#if action_has_next>, + + ] + } +} + + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dodcustomtypes.get.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dodcustomtypes.get.desc.xml new file mode 100644 index 0000000000..bb5f689ba8 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dodcustomtypes.get.desc.xml @@ -0,0 +1,12 @@ + + Records Management DOD 5015 Custom Types + + ]]> + + /api/rma/admin/dodcustomtypes + + user + required + internal + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dodcustomtypes.get.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dodcustomtypes.get.json.ftl new file mode 100644 index 0000000000..2a70d90714 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/dodcustomtypes.get.json.ftl @@ -0,0 +1,16 @@ +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "data": + { + "dodCustomTypes": + [ + <#list dodCustomTypes as aspDef> + { + "name": "${aspDef.name.prefixString}", + "title": "${aspDef.title!""}" + }<#if aspDef_has_next>, + + ] + } +} + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/export.post.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/export.post.desc.xml new file mode 100644 index 0000000000..a9f50f435c --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/export.post.desc.xml @@ -0,0 +1,16 @@ + + Records Management Export + + The body of the post should be in the form
+ {
+    "nodeRefs" : array of nodeRefs to export
+ }
+ ]]> +
+ /api/rma/admin/export + argument + user + required + internal +
\ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/export.post.html.status.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/export.post.html.status.ftl new file mode 100644 index 0000000000..6c0811ac9a --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/export.post.html.status.ftl @@ -0,0 +1,19 @@ + + + Export failure + + +<#if (args.failureCallbackFunction?exists)> + + + + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/fileplanreport.get.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/fileplanreport.get.desc.xml new file mode 100644 index 0000000000..4f99e04187 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/fileplanreport.get.desc.xml @@ -0,0 +1,13 @@ + + Get Fileplan Report + Returns STATUS_OK (200) + ]]> + + /api/node/{store_type}/{store_id}/{id}/fileplanreport + + user + required + draft_public_api + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/fileplanreport.get.js b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/fileplanreport.get.js new file mode 100644 index 0000000000..87e25f8955 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/fileplanreport.get.js @@ -0,0 +1,188 @@ +/** + * Main entry point for this webscript. + * Builds a nodeRef from the url and creates a records series, category and/or folder + * template model depending on what nodeRef that has been given. + * + * @method main + */ +function main() +{ + // Get the node from the URL + var pathSegments = url.match.split("/"); + var reference = [ url.templateArgs.store_type, url.templateArgs.store_id ].concat(url.templateArgs.id.split("/")); + var node = search.findNode(pathSegments[2], reference); + + // 404 if the node is not found + if (node == null) + { + status.setCode(status.STATUS_NOT_FOUND, "The node could not be found"); + return; + } + + // Get the record series, categories and/or folders + if(node.type == "{http://www.alfresco.org/model/dod5015/1.0}filePlan") + { + var recordSeries = [], + seriesNodes = node.children, + seriesNode; + for (var rsi = 0, rsl = seriesNodes.length; rsi < rsl; rsi++) + { + var seriesNode = seriesNodes[rsi]; + if(seriesNode.type == "{http://www.alfresco.org/model/dod5015/1.0}recordSeries") + { + recordSeries.push(getRecordSeries(seriesNode)); + } + } + recordSeries.sort(sortByName); + model.recordSeries = recordSeries; + } + else if(node.type == "{http://www.alfresco.org/model/dod5015/1.0}recordSeries") + { + var recordSeries = []; + recordSeries.push(getRecordSeries(node)); + model.recordSeries = recordSeries; + } + else if(node.type == "{http://www.alfresco.org/model/dod5015/1.0}recordCategory") + { + var recordCategories = []; + recordCategories.push(getRecordCategory(node, "/" + node.parent.name + "/")); + model.recordCategories = recordCategories; + } + else if(node.type == "{http://www.alfresco.org/model/recordsmanagement/1.0}recordFolder") + { + var recordFolders = []; + var recordCategory = node.parent; + recordFolders.push(getRecordFolder(node, "" + recordCategory.parent.name + "/" + recordCategory.name + "/")); + model.recordFolders = recordFolders; + } +} + +/** + * Sort helper function for objects with names + * + * @method sortByName + * @param obj1 + * @param obj2 + */ +function sortByName(obj1, obj2) +{ + return (obj1.name > obj2.name) ? 1 : (obj1.name < obj2.name) ? -1 : 0; +} + +/** + * Takes a ScriptNode and builds a Record Series template model from it + * + * @method getRecordSeries + * @param seriesNode {ScriptNode} A ScriptNode of type "rma:recordSeries" + */ +function getRecordSeries(seriesNode) +{ + // Create Record Series object + var recordSerie = { + parentPath: "/", + name: seriesNode.name, + identifier: seriesNode.properties["rma:identifier"], + description: seriesNode.properties["description"] + }; + + // Find all Record Categories + var recordCategories = [], + categoryNodes = seriesNode.children, + categoryNode; + for (var rci = 0, rcl = categoryNodes.length; rci < rcl; rci++) + { + categoryNode = categoryNodes[rci]; + if(categoryNode.type == "{http://www.alfresco.org/model/dod5015/1.0}recordCategory") + { + // Create and add Record Category object + recordCategories.push(getRecordCategory(categoryNode, "/" + seriesNode.name + "/")); + } + } + recordCategories.sort(sortByName); + recordSerie.recordCategories = recordCategories; + + // Return Record Series + return recordSerie; +} + +/** + * Takes a ScriptNode and builds a Record Category template model from it + * + * @method getRecordCategory + * @param categoryNode {ScriptNode} A ScriptNode of type "rma:recordCategory" + * @param parentPath {string} The file path starting from the top of the fileplan + */ +function getRecordCategory(categoryNode, parentPath) +{ + // Create Record Category object + var recordCategory = { + parentPath: parentPath, + name: categoryNode.name, + identifier: categoryNode.properties["rma:identifier"], + vitalRecordIndicator: categoryNode.properties["vitalRecordIndicator"], + dispositionAuthority: categoryNode.properties["dispositionAuthority"], + recordFolders: [], + dispositionActions: [] + }; + + // Find all Record Folders & Disposition information + var recordFolders = [], + dispositionActions = [], + categoryChildren = categoryNode.children, + categoryChild, + dispScheduleChildren, + dispScheduleChild; + for (var cci = 0, ccil = categoryChildren.length; cci < ccil; cci++) + { + categoryChild = categoryChildren[cci] + if (categoryChild.type == "{http://www.alfresco.org/model/recordsmanagement/1.0}recordFolder") + { + // Create and add Record Folder object + recordFolders.push(getRecordFolder(categoryChild, parentPath + categoryNode.name + "/")); + } + else if (categoryChild.type == "{http://www.alfresco.org/model/recordsmanagement/1.0}dispositionSchedule") + { + // Get Disposition authority + recordCategory.dispositionAuthority = categoryChild.properties["rma:dispositionAuthority"]; + dispScheduleChildren = categoryChild.children; + for (var dsi = 0, dsil = dispScheduleChildren.length; dsi < dsil; dsi++) + { + dispScheduleChild = dispScheduleChildren[dsi]; + if (dispScheduleChild.type == "{http://www.alfresco.org/model/recordsmanagement/1.0}dispositionActionDefinition") + { + // Add Disposition Action description + dispositionActions.push({ + dispositionDescription: dispScheduleChild.properties["rma:dispositionDescription"] + }); + } + } + } + } + + // Add Record Category to the list + recordFolders.sort(sortByName); + recordCategory.recordFolders = recordFolders; + recordCategory.dispositionActions = dispositionActions; + return recordCategory; +} + +/** + * Takes a ScriptNode and builds a Record Category template model from it + * + * @method getRecordFolder + * @param recordFolder {ScriptNode} A ScriptNode of type "rma:recordrecordFolder" + * @param parentPath {string} The file path starting from the top of the fileplan + */ +function getRecordFolder(recordFolder, parentPath) +{ + return { + parentPath: parentPath, + name: recordFolder.name, + identifier: recordFolder.properties["rma:identifier"], + vitalRecordIndicator: recordFolder.properties["vitalRecordIndicator"] + }; +} + +// Start webscript +main(); + diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/fileplanreport.get.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/fileplanreport.get.json.ftl new file mode 100644 index 0000000000..90a4bf1c06 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/fileplanreport.get.json.ftl @@ -0,0 +1,19 @@ +<#import "fileplanreport.lib.ftl" as reportLib/> +<#macro dateFormat date>${date?string("dd MMM yyyy HH:mm:ss 'GMT'Z '('zzz')'")} +<#escape x as jsonUtils.encodeJSONString(x)> +{ + data: + { + "firstName": <#if person.properties.firstName??>"${person.properties.firstName}"<#else>null, + "lastName": <#if person.properties.lastName??>"${person.properties.lastName}"<#else>null, + <#if (recordSeries??)> + "recordSeries": <@reportLib.recordSeriesJSON recordSeries=recordSeries/>, + <#elseif (recordCategories??)> + "recordCategories": <@reportLib.recordCategoriesJSON recordCategories=recordCategories/>, + <#elseif (recordFolders??)> + "recordFolders": <@reportLib.recordFoldersJSON recordFolders=recordFolders/>, + + "printDate": "<@dateFormat date=date/>" + } +} + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/fileplanreport.lib.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/fileplanreport.lib.ftl new file mode 100644 index 0000000000..fb5c18680f --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/fileplanreport.lib.ftl @@ -0,0 +1,46 @@ +<#macro recordSeriesJSON recordSeries> +<#escape x as jsonUtils.encodeJSONString(x)> + [<#list recordSeries as recordSerie> + { + "parentPath": "${recordSerie.parentPath}", + "name": "${recordSerie.name}", + "identifier": "${recordSerie.identifier}", + "description": "${recordSerie.description}", + "recordCategories": <@recordCategoriesJSON recordCategories=recordSerie.recordCategories/> + }<#if (recordSerie_has_next)>, + ] + + + +<#macro recordCategoriesJSON recordCategories> +<#escape x as jsonUtils.encodeJSONString(x)> + [<#list recordCategories as recordCategory> + { + "parentPath": "${recordCategory.parentPath}", + "name": "${recordCategory.name}", + "identifier": "${recordCategory.identifier}", + <#if (recordCategory.vitalRecordIndicator??)>"vitalRecordIndicator": ${recordCategory.vitalRecordIndicator?string}, + <#if (recordCategory.dispositionAuthority??)>"dispositionAuthority": "${recordCategory.dispositionAuthority}", + "recordFolders": <@recordFoldersJSON recordFolders=recordCategory.recordFolders/>, + "dispositionActions": [<#list recordCategory.dispositionActions as dispositionAction> + { + "dispositionDescription": "${dispositionAction.dispositionDescription!""}" + }<#if (dispositionAction_has_next)>, + ] + }<#if (recordCategory_has_next)>, + ] + + + +<#macro recordFoldersJSON recordFolders> +<#escape x as jsonUtils.encodeJSONString(x)> + [<#list recordFolders as recordFolder> + { + "parentPath": "${recordFolder.parentPath}", + "name": "${recordFolder.name}", + "identifier": "${recordFolder.identifier}", + <#if (recordFolder.vitalRecordIndicator??)>"vitalRecordIndicator": "${recordFolder.vitalRecordIndicator?string}" + }<#if (recordFolder_has_next)>, + ] + + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/import.post.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/import.post.desc.xml new file mode 100644 index 0000000000..bb2c537379 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/import.post.desc.xml @@ -0,0 +1,17 @@ + + Records Management Import + + The body of the post should be multipart/form-data and contain the following fields.
+
    +
  • destination: array of nodeRefs to export
  • +
  • archive: array of nodeRefs to export
  • +
+ ]]> +
+ /api/rma/admin/import + + user + required + internal +
\ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/import.post.html.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/import.post.html.ftl new file mode 100644 index 0000000000..ff0d596e2d --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/import.post.html.ftl @@ -0,0 +1,14 @@ + + + Upload success + + +<#if (args.successCallback?exists)> + + + + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/import.post.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/import.post.json.ftl new file mode 100644 index 0000000000..1ef9fab7d9 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/import.post.json.ftl @@ -0,0 +1,5 @@ +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "success": ${success?string} +} + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/listofvalues.get.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/listofvalues.get.desc.xml new file mode 100644 index 0000000000..9b216d0058 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/listofvalues.get.desc.xml @@ -0,0 +1,9 @@ + + List Of Values + Returns lists of items used by the Records Management service + /api/rma/admin/listofvalues + argument + user + required + internal + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/listofvalues.get.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/listofvalues.get.json.ftl new file mode 100644 index 0000000000..a84e447879 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/listofvalues.get.json.ftl @@ -0,0 +1,2 @@ +<#import "listofvalues.lib.ftl" as listsLib/> +<@listsLib.listsJSON lists=lists/> \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/listofvalues.lib.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/listofvalues.lib.ftl new file mode 100644 index 0000000000..ae50014424 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/listofvalues.lib.ftl @@ -0,0 +1,75 @@ +<#macro listsJSON lists> +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "data": + { + "dispositionActions": + { + "url": "${lists.dispositionActions.url}", + "items": + [ + <#list lists.dispositionActions.items as item> + { + "label": "${item.label}", + "value": "${item.value}" + }<#if item_has_next>, + + ] + }, + "events": + { + "url": "${lists.events.url}", + "items": + [ + <#list lists.events.items as item> + { + "label": "${item.label}", + "value": "${item.value}", + "automatic": ${item.automatic?string} + }<#if item_has_next>, + + ] + }, + "periodTypes": + { + "url": "${lists.periodTypes.url}", + "items": + [ + <#list lists.periodTypes.items as item> + { + "label": "${item.label}", + "value": "${item.value}" + }<#if item_has_next>, + + ] + }, + "periodProperties": + { + "url": "${lists.periodProperties.url}", + "items": + [ + <#list lists.periodProperties.items as item> + { + "label": "${item.label}", + "value": "${item.value}" + }<#if item_has_next>, + + ] + }, + "auditEvents": + { + "url": "${lists.auditEvents.url}", + "items": + [ + <#list lists.auditEvents.items as item> + { + "label": "${item.label}", + "value": "${item.value}" + }<#if item_has_next>, + + ] + } + } +} + + diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/recordmetadataaspects.get.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/recordmetadataaspects.get.desc.xml new file mode 100644 index 0000000000..14acb48915 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/recordmetadataaspects.get.desc.xml @@ -0,0 +1,12 @@ + + Record Metadata Aspects + + + /api/rma/recordmetadataaspects + + user + required + internal + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/recordmetadataaspects.get.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/recordmetadataaspects.get.json.ftl new file mode 100644 index 0000000000..e83ef198fd --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/recordmetadataaspects.get.json.ftl @@ -0,0 +1,17 @@ +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "data": + { + "recordMetaDataAspects": + [ + <#list aspects as aspect> + { + "id" : "${aspect.id}", + "value" : "${aspect.value}" + } + <#if aspect_has_next>, + + ] + } +} + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmaction.post.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmaction.post.desc.xml new file mode 100644 index 0000000000..7f4a7f1dd6 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmaction.post.desc.xml @@ -0,0 +1,19 @@ + + Records Management Action Execution + + The body of the post should be in the form
+ {
+    "nodeRef" : nodeRef for target Record,
+    "nodeRefs" : array of nodeRef for target Records (either this or "nodeRef" should be present),
+    "name" : actionName,
+    "params" : {actionParameters}
+ }
+ ]]> +
+ /api/rma/actions/ExecutionQueue + + user + required + internal +
\ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmaction.post.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmaction.post.json.ftl new file mode 100644 index 0000000000..ed22ad29fd --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmaction.post.json.ftl @@ -0,0 +1,14 @@ +{ + "message" : "${message}" +<#if result?exists> + ,"result" : "${result?string}" + +<#if results?exists> + ,"results" : + { + <#list results?keys as prop> + "${prop}" : "${results[prop]}"<#if prop_has_next>, + + } + +} \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmauditlog.delete.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmauditlog.delete.desc.xml new file mode 100644 index 0000000000..8cf91b17a0 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmauditlog.delete.desc.xml @@ -0,0 +1,9 @@ + + Clears Records Management Audit Log + Clears the Records Management audit log + /api/rma/admin/rmauditlog + argument + user + required + internal + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmauditlog.delete.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmauditlog.delete.json.ftl new file mode 100644 index 0000000000..9f0631dd99 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmauditlog.delete.json.ftl @@ -0,0 +1,2 @@ +<#import "rmauditlog.lib.ftl" as auditLib/> +<@auditLib.auditStatusJSON auditstatus=auditstatus/> \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmauditlog.get.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmauditlog.get.desc.xml new file mode 100644 index 0000000000..0f76ff2953 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmauditlog.get.desc.xml @@ -0,0 +1,22 @@ + + Records Management Audit Log + The following parameters can also be passed: +
    +
  • size: Maximum number of log entries to return
  • +
  • user: Only return log entries by the specified user
  • +
  • event: Only return log entries matching this event
  • +
  • from: Only return log entries after the specified date, date should be in yyyy-MM-dd format
  • +
  • to: Only return log entries before the specified date, date should be in yyyy-MM-dd format
  • +
  • export: Set this to 'true' to force the browser to display the Save As dialog
  • +
+ ]]> +
+ /api/rma/admin/rmauditlog + /api/node/{store_type}/{store_id}/{id}/rmauditlog + argument + user + required + internal +
\ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmauditlog.lib.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmauditlog.lib.ftl new file mode 100644 index 0000000000..dd270cebca --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmauditlog.lib.ftl @@ -0,0 +1,12 @@ +<#macro auditStatusJSON auditstatus> +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "data": + { + "enabled": ${auditstatus.enabled?string}, + "started": "${auditstatus.started}", + "stopped": "${auditstatus.stopped}" + } +} + + diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmauditlog.post.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmauditlog.post.desc.xml new file mode 100644 index 0000000000..d0ddd80ac3 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmauditlog.post.desc.xml @@ -0,0 +1,23 @@ + + Files a Records Management audit log as a record + + A JSON structure is expected as follows:
+ {
+    "destination" : NodeRef of record folder to file the audit log in
+    "size" : Maximum number of log entries to return
+    "user" : Only return log entries by the specified user
+    "event" : Only return log entries matching this event
+    "from" : Only return log entries after the specified date, date should be in yyyy-MM-dd format
+    "to" : Only return log entries before the specified date, date should be in yyyy-MM-dd format
+ }
+ ]]> +
+ /api/rma/admin/rmauditlog + /api/node/{store_type}/{store_id}/{id}/rmauditlog + argument + user + required + internal +
\ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmauditlog.put.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmauditlog.put.desc.xml new file mode 100644 index 0000000000..e13de43b34 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmauditlog.put.desc.xml @@ -0,0 +1,16 @@ + + Start or Stop Records Management Audit Log + The body of the put should be in the form
+ {
+    "enabled" : true|false
+ }
+ ]]> +
+ /api/rma/admin/rmauditlog + argument + user + required + internal +
\ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmauditlog.put.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmauditlog.put.json.ftl new file mode 100644 index 0000000000..9f0631dd99 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmauditlog.put.json.ftl @@ -0,0 +1,2 @@ +<#import "rmauditlog.lib.ftl" as auditLib/> +<@auditLib.auditStatusJSON auditstatus=auditstatus/> \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmauditlogstatus.get.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmauditlogstatus.get.desc.xml new file mode 100644 index 0000000000..3d32335110 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmauditlogstatus.get.desc.xml @@ -0,0 +1,12 @@ + + Records Management Audit Log Status + + + /api/rma/admin/rmauditlog/status + argument + user + required + internal + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmauditlogstatus.get.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmauditlogstatus.get.json.ftl new file mode 100644 index 0000000000..bbb76957b5 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmauditlogstatus.get.json.ftl @@ -0,0 +1,6 @@ +{ + "data" : + { + "enabled" : ${enabled?string} + } +} \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmconstraints.get.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmconstraints.get.desc.xml new file mode 100644 index 0000000000..445dc99bf0 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmconstraints.get.desc.xml @@ -0,0 +1,14 @@ + + Get the allowed values for the authenticated user for an rm list constraint. + + listName is the qualified name of the list with the ":" replaced by "_" eg rmc_smList + ]]> + + /api/rma/rmconstraints/{listName} + argument + user + required + internal + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmconstraints.get.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmconstraints.get.json.ftl new file mode 100644 index 0000000000..995b7e3c48 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmconstraints.get.json.ftl @@ -0,0 +1,15 @@ +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "data": { + "constraintName": "${constraintName}", + "allowedValuesForCurrentUser" : [ + <#list allowedValuesForCurrentUser as item> + { + "label": "${item}", + "value": "${item}" + }<#if item_has_next>, + + ] + } +} + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmpermissions.get.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmpermissions.get.desc.xml new file mode 100644 index 0000000000..cd90572016 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmpermissions.get.desc.xml @@ -0,0 +1,9 @@ + + Records Management Permissions + Retrieve the Permissions set against a Records Management node. + /api/node/{store_type}/{store_id}/{id}/rmpermissions + argument + user + required + internal + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmpermissions.get.js b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmpermissions.get.js new file mode 100644 index 0000000000..439a2654ac --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmpermissions.get.js @@ -0,0 +1,86 @@ +/** + * Entry point for rmpermissions GET data webscript. + * Queries the permissions from an RM node and constructs the data-model for the template. + * + * @method main + */ +function main() +{ + // Get the node from the URL + var pathSegments = url.match.split("/"); + var reference = [ url.templateArgs.store_type, url.templateArgs.store_id ].concat(url.templateArgs.id.split("/")); + var node = search.findNode(pathSegments[2], reference); + + // 404 if the node is not found + if (node == null) + { + status.setCode(status.STATUS_NOT_FOUND, "The node could not be found"); + return; + } + + // retrieve permissions applied to this node + var permissions = node.getFullPermissions(); + + // split tokens - results are in the format: + // [ALLOWED|DENIED];[USERNAME|GROUPNAME];PERMISSION;[INHERITED|DIRECT] + var result = []; + for (var i=0; i +{ + "data": + { + "permissions": + [ + <#list permissions as perm> + { + "id": "${perm.id}", + "authority": + { + "id": "${perm.authority.id}", + "label": "${perm.authority.label}" + }, + "inherited": ${perm.inherited?string} + }<#if perm_has_next>, + + ], + "inherited": ${inherited?string} + } +} + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmpermissions.post.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmpermissions.post.desc.xml new file mode 100644 index 0000000000..e96bac280c --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmpermissions.post.desc.xml @@ -0,0 +1,36 @@ + + Apply Records Management Permissions to a node + +
+ The body of the post json should be of the form: +
+   {
+      "permissions":
+      [
+         {
+            "id": "Filing",
+            "authority": "GROUP_Administrator"
+         },
+         {
+            "id": "ReadRecords",
+            "authority": "userxyz"
+            "remove": true
+         },
+         ...
+      ]
+   }
+   
+ Existing permissions will be updated by the supplied permission set, + where 'id' and 'authority' are mandatory values.
+ If the optional 'remove' flag is set then the permission will be removed. + Note that it is only valid to set the following RM related permissions: + 'Filing', 'ReadRecords' and 'FileRecords'. + ]]> +
+ /api/node/{store_type}/{store_id}/{id}/rmpermissions + argument + user + required + internal +
\ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmpermissions.post.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmpermissions.post.json.ftl new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmpermissions.post.json.ftl @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmpermissions.post.json.js b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmpermissions.post.json.js new file mode 100644 index 0000000000..6aa731567e --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/rmpermissions.post.json.js @@ -0,0 +1,52 @@ +/** + * Entry point for rmpermissions POST data webscript. + * Applies supplied RM permissions to an RM node. + * + * @method main + */ +function main() +{ + // Get the node from the URL + var pathSegments = url.match.split("/"); + var reference = [ url.templateArgs.store_type, url.templateArgs.store_id ].concat(url.templateArgs.id.split("/")); + var node = search.findNode(pathSegments[2], reference); + + // 404 if the node is not found + if (node == null) + { + status.setCode(status.STATUS_NOT_FOUND, "The node could not be found"); + return; + } + + if (json.has("permissions") == false) + { + status.setCode(status.STATUS_BAD_REQUEST, "Permissions value missing from request."); + } + + var permissions = json.getJSONArray("permissions"); + for (var i=0; i + Records Management Transfer + Streams an Alfresco Content Pacakge (ACP) file containing the contents of a transfer + /api/node/{store_type}/{store_id}/{id}/transfers/{transfer_id} + argument + user + required + internal + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/transferreport.get.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/transferreport.get.desc.xml new file mode 100644 index 0000000000..727c0c7a6a --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/transferreport.get.desc.xml @@ -0,0 +1,9 @@ + + Records Management Transfer Report + Returns a transfer report to the caller in JSON format + /api/node/{store_type}/{store_id}/{id}/transfers/{transfer_id}/report + argument + user + required + internal + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/transferreport.post.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/transferreport.post.desc.xml new file mode 100644 index 0000000000..37c74d9a73 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/transferreport.post.desc.xml @@ -0,0 +1,16 @@ + + Files a Records Management Transfer Report + A JSON structure is expected as follows:
+ {
+    "destination" : NodeRef of record folder to file the transfer report in
+ }
+ ]]> +
+ /api/node/{store_type}/{store_id}/{id}/transfers/{transfer_id}/report + argument + user + required + internal +
\ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/userrightsreport.get.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/userrightsreport.get.desc.xml new file mode 100644 index 0000000000..d0151a4915 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/userrightsreport.get.desc.xml @@ -0,0 +1,9 @@ + + Records Management User Rights Report + Returns a user rights report showing users, roles and groups to the caller in JSON format + /api/rma/admin/userrightsreport + argument + user + required + internal + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/userrightsreport.get.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/userrightsreport.get.json.ftl new file mode 100644 index 0000000000..81e88674a3 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/rma/userrightsreport.get.json.ftl @@ -0,0 +1,46 @@ +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "data": + { + "users": + { + <#list report.users?keys as user> + "${user}": + { + "userName": "${report.users[user].userName!""}", + "firstName": "${report.users[user].firstName!""}", + "lastName": "${report.users[user].lastName!""}", + "roles": [<#list report.users[user].roles as role>"${role}"<#if role_has_next>,], + "groups": [<#list report.users[user].groups as group>"${group}"<#if group_has_next>,] + } + <#if user_has_next>, + + }, + "roles": + { + <#list report.roles?keys as role> + "${role}": + { + "name": "${report.roles[role].name!""}", + "label": "${report.roles[role].displayLabel!""}", + "users": [<#list report.roles[role].users as user>"${user}"<#if user_has_next>,], + "capabilities": [<#list report.roles[role].capabilities as capability>"${capability}"<#if capability_has_next>,] + } + <#if role_has_next>, + + }, + "groups": + { + <#list report.groups?keys as group> + "${group}": + { + "name": "${report.groups[group].name!""}", + "label": "${report.groups[group].displayLabel!""}", + "users": [<#list report.groups[group].users as user>"${user}"<#if user_has_next>,] + } + <#if group_has_next>, + + } + } +} + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary-v2/rm-doclist.get.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary-v2/rm-doclist.get.desc.xml new file mode 100644 index 0000000000..8b8441f74d --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary-v2/rm-doclist.get.desc.xml @@ -0,0 +1,12 @@ + + doclist-v2 + Document List v2 Component - records management doclist data webscript + /slingshot/doclib2/rm/doclist/{type}/site/{site}/{container}/{path} + /slingshot/doclib2/rm/doclist/{type}/site/{site}/{container} + /slingshot/doclib2/rm/doclist/{type}/node/{store_type}/{store_id}/{id}/{path} + /slingshot/doclib2/rm/doclist/{type}/node/{store_type}/{store_id}/{id} + argument + user + required + internal + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary-v2/rm-doclist.get.js b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary-v2/rm-doclist.get.js new file mode 100644 index 0000000000..ac91752be0 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary-v2/rm-doclist.get.js @@ -0,0 +1,10 @@ + + + + + + +/** + * Document List Component: doclist + */ +model.doclist = doclist_main(); diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary-v2/rm-doclist.get.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary-v2/rm-doclist.get.json.ftl new file mode 100644 index 0000000000..df84dfc21e --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary-v2/rm-doclist.get.json.ftl @@ -0,0 +1 @@ +<#include "doclist.get.json.ftl"> \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary-v2/rm-filters.lib.js b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary-v2/rm-filters.lib.js new file mode 100644 index 0000000000..25a0234d47 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary-v2/rm-filters.lib.js @@ -0,0 +1,197 @@ +/** +* Query templates for custom search +*/ +Filters.QUERY_TEMPLATES = +[ + { field: "keywords", template: "%(cm:name cm:title cm:description TEXT)" }, + { field: "name", template: "%(cm:name)" }, + { field: "title", template: "%(cm:title)" }, + { field: "description", template: "%(cm:description)" }, + { field: "creator", template: "%(cm:creator)" }, + { field: "created", template: "%(cm:created)" }, + { field: "modifier", template: "%(cm:modifier)" }, + { field: "modified", template: "%(cm:modified)" }, + { field: "author", template: "%(cm:author)" }, + { field: "markings", template: "%(rmc:supplementalMarkingList)" }, + { field: "dispositionEvents", template: "%(rma:recordSearchDispositionEvents)" }, + { field: "dispositionActionName", template: "%(rma:recordSearchDispositionActionName)" }, + { field: "dispositionActionAsOf", template: "%(rma:recordSearchDispositionActionAsOf)" }, + { field: "dispositionEventsEligible", template: "%(rma:recordSearchDispositionEventsEligible)" }, + { field: "dispositionPeriod", template: "%(rma:recordSearchDispositionPeriod)" }, + { field: "hasDispositionSchedule", template: "%(rma:recordSearchHasDispositionSchedule)" }, + { field: "dispositionInstructions", template: "%(rma:recordSearchDispositionInstructions)" }, + { field: "dispositionAuthority", template: "%(rma:recordSearchDispositionAuthority)" }, + { field: "holdReason", template: "%(rma:recordSearchHoldReason)" }, + { field: "vitalRecordReviewPeriod", template: "%(rma:recordSearchVitalRecordReviewPeriod)" } +]; + +Filters.IGNORED_TYPES = +[ + /* Defaults */ + "cm:systemfolder", + "fm:forums", + "fm:forum", + "fm:topic", + "fm:post", + /* Records Management */ + "rma:dispositionSchedule", + "rma:dispositionActionDefinition", + "rma:dispositionAction", + "rma:hold", + "rma:transfer" +]; + +/** + * Create filter parameters based on input parameters + * + * @method getFilterParams + * @param filter {string} Required filter + * @param parsedArgs {object} Parsed arguments object literal + * @param optional {object} Optional arguments depending on filter type + * @return {object} Object literal containing parameters to be used in Lucene search + */ +Filters.getFilterParams = function RecordsManagementFilter_getFilterParams(filter, parsedArgs, optional) +{ + var filterParams = + { + query: "+PATH:\"" + parsedArgs.pathNode.qnamePath + "/*\"", + limitResults: null, + sort: [ + { + column: "@cm:name", + ascending: true + }], + language: "lucene", + templates: null, + variablePath: true, + ignoreTypes: Filters.IGNORED_TYPES + }; + + 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)) + { + filterParams.limitResults = argMax; + } + + // Create query based on passed-in arguments + var filterData = args.filterData, + filterQuery = ""; + + // Common types and aspects to filter from the UI + var filterQueryDefaults = ' -TYPE:"' + Filters.IGNORED_TYPES.join('" -TYPE:"') + '"'; + + // Create query based on passed-in arguments + switch (String(filter)) + { + case "all": + filterQuery = "+PATH:\"" + parsedArgs.rootNode.qnamePath + "//*\""; + filterQuery += " -TYPE:\"{http://www.alfresco.org/model/content/1.0}folder\""; + filterParams.query = filterQuery + filterQueryDefaults; + break; + + case "node": + parsedArgs.pathNode = parsedArgs.rootNode.parent; + filterParams.variablePath = false; + filterParams.query = "+ID:\"" + parsedArgs.rootNode.nodeRef + "\""; + break; + + case "savedsearch": + var searchNode = parsedArgs.location.siteNode.getContainer("Saved Searches"); + if (searchNode != null) + { + var ssNode = searchNode.childByNamePath(String(filterData)); + + if (ssNode != null) + { + var ssJson = eval('try{(' + ssNode.content + ')}catch(e){}'); + filterQuery = ssJson.query; + // Wrap the query so that only valid items within the filePlan are returned + filterParams.query = 'PATH:"' + parsedArgs.rootNode.qnamePath + '//*" AND (' + filterQuery + ')'; + filterParams.templates = Filters.QUERY_TEMPLATES; + filterParams.language = "fts-alfresco"; + filterParams.namespace = "http://www.alfresco.org/model/recordsmanagement/1.0"; + // gather up the sort by fields + // they are encoded as "property/dir" i.e. "cm:name/asc" + if (ssJson.sort && ssJson.sort.length !== 0) + { + var sortPairs = ssJson.sort.split(","); + var sort = []; + for (var i=0, j; i a.properties["rma:dispositionActionCompletedAt"] ? 1 : -1); + }; + + if (history != null) + { + history.sort(fnSortByCompletionDateReverse); + previous = history[0]; + } + + return previous; + }, + + /** + * Record and Record Folder common evaluators + */ + recordAndRecordFolder: function Evaluator_recordAndRecordFolder(asset, permissions, status) + { + var actionName = asset.properties["rma:recordSearchDispositionActionName"], + actionAsOf = asset.properties["rma:recordSearchDispositionActionAsOf"], + hasNextAction = asset.childAssocs["rma:nextDispositionAction"] != null, + recentHistory = Evaluator.getPreviousDispositionAction(asset), + previousAction = null, + now = new Date(); + + /* Next Disposition Action */ + // Next action could become eligible based on asOf date + if (actionAsOf != null) + { + if (hasNextAction) + { + permissions["disposition-as-of"] = true; + } + + // Check if action asOf date has passed + if (actionAsOf < now) + { + permissions[actionName] = true; + } + } + // Next action could also become eligible based on event completion + if (asset.properties["rma:recordSearchDispositionEventsEligible"] == true) + { + permissions[actionName] = true; + } + + /* Previous Disposition Action */ + if (recentHistory != null) + { + previousAction = recentHistory.properties["rma:dispositionAction"]; + } + + /* Cut Off status */ + if (asset.hasAspect("rma:cutOff")) + { + status["cutoff"] = true; + if (asset.hasAspect("rma:dispositionLifecycle")) + { + if (previousAction == "cutoff") + { + permissions["undo-cutoff"] = true; + delete permissions["cutoff"]; + } + } + } + + /* Transfer or Accession Pending Completion */ + // Don't show transfer or accession if either is pending completion + var assocs = asset.parentAssocs["rma:transferred"]; + if (actionName == "transfer" && assocs != null && assocs.length > 0) + { + delete permissions["transfer"]; + delete permissions["undo-cutoff"]; + delete permissions["disposition-as-of"]; + status["transfer " + assocs[0].name] = true; + } + assocs = asset.parentAssocs["rma:ascended"]; + if (actionName == "accession" && assocs != null && assocs.length > 0) + { + delete permissions["accession"]; + delete permissions["undo-cutoff"]; + delete permissions["disposition-as-of"]; + status["accession " + assocs[0].name] = true; + } + + /* Transferred status */ + if (asset.hasAspect("rma:transferred")) + { + var transferLocation = ""; + if (previousAction == "transfer") + { + var actionId = recentHistory.properties["rma:dispositionActionId"], + actionNode = search.findNode("workspace://SpacesStore/" + actionId); + + if (actionNode != null && actionNode.properties["rma:dispositionLocation"]) + { + transferLocation = " " + actionNode.properties["rma:dispositionLocation"]; + } + } + status["transferred" + transferLocation] = true; + } + + /* Accessioned status */ + if (asset.hasAspect("rma:ascended")) + { + status["accessioned NARA"] = true; + } + + /* Review As Of Date */ + if (asset.hasAspect("rma:vitalRecord")) + { + if (asset.properties["rma:reviewAsOf"] != null) + { + permissions["review-as-of"] = true; + } + } + + /* Frozen/Unfrozen */ + if (asset.hasAspect("rma:frozen")) + { + status["frozen"] = true; + if (permissions["Unfreeze"]) + { + permissions["unfreeze"] = true; + } + } + else + { + if (permissions["ExtendRetentionPeriodOrFreeze"]) + { + permissions["freeze"] = true; + } + } + }, + + /** + * Record Type evaluator + */ + recordType: function Evaluator_recordType(asset) + { + /* Supported Record Types */ + var recordTypes = + [ + "digitalPhotographRecord", + "pdfRecord", + "scannedRecord", + "webRecord" + ], + currentRecordType = null; + + for (var i = 0; i < recordTypes.length; i++) + { + if (asset.hasAspect("dod:" + recordTypes[i])) + { + currentRecordType = recordTypes[i]; + break; + } + } + + return currentRecordType; + }, + + /** + * Asset Evaluator - main entrypoint + */ + run: function Evaluator_run(asset, capabilitySet) + { + var assetType = Evaluator.getAssetType(asset), + rmNode, + recordType = null, + capabilities = {}, + actions = {}, + actionSet = "empty", + permissions = {}, + status = {}, + suppressRoles = false; + + var now = new Date(); + + try + { + rmNode = rmService.getRecordsManagementNode(asset) + } + catch (e) + { + // Not a Records Management Node + return null; + } + + /** + * Capabilities and Actions + */ + var caps, cap, act; + if (capabilitySet == "all") + { + caps = rmNode.capabilities; + } + else + { + caps = rmNode.capabilitiesSet(capabilitySet); + } + + for each (cap in caps) + { + capabilities[cap.name] = true; + for each (act in cap.actions) + { + actions[act] = true; + } + } + + /** + * COMMON FOR ALL TYPES + */ + + /** + * Basic permissions - start from entire capabiltiies list + * TODO: Filter-out the ones not relevant to DocLib UI. + */ + permissions = capabilities; + + /** + * Multiple parent assocs + */ + var parents = asset.parentAssocs["contains"]; + if (parents !== null && parents.length > 1) + { + status["multi-parent " + parents.length] = true; + } + + /** + * E-mail type + */ + if (asset.mimetype == "message/rfc822") + { + permissions["split-email"] = true; + } + + switch (assetType) + { + /** + * SPECIFIC TO: FILE PLAN + */ + case "fileplan": + permissions["new-series"] = capabilities["Create"]; + break; + + + /** + * SPECIFIC TO: RECORD SERIES + */ + case "record-series": + actionSet = "recordSeries"; + permissions["new-category"] = capabilities["Create"]; + break; + + + /** + * SPECIFIC TO: RECORD CATEGORY + */ + case "record-category": + actionSet = "recordCategory"; + permissions["new-folder"] = capabilities["Create"]; + break; + + + /** + * SPECIFIC TO: RECORD FOLDER + */ + case "record-folder": + actionSet = "recordFolder"; + + /* Record and Record Folder common evaluator */ + Evaluator.recordAndRecordFolder(asset, permissions, status); + + /* Update Cut Off status to folder-specific status */ + if (status["cutoff"] == true) + { + delete status["cutoff"]; + status["cutoff-folder"] = true; + } + + /* File new Records */ + permissions["file"] = capabilities["Create"]; + + /* Open/Closed */ + if (asset.properties["rma:isClosed"]) + { + // Cutoff implies closed, so no need to duplicate + if (!status["cutoff-folder"]) + { + status["closed"] = true; + } + if (capabilities["ReOpenFolders"]) + { + permissions["open-folder"] = true; + } + } + else + { + status["open"] = true; + if (capabilities["CloseFolders"]) + { + permissions["close-folder"] = true; + } + } + break; + + + /** + * SPECIFIC TO: RECORD + */ + case "record": + actionSet = "record"; + + /* Record and Record Folder common evaluator */ + Evaluator.recordAndRecordFolder(asset, permissions, status); + + /* Electronic/Non-electronic documents */ + if (asset.typeShort == "rma:nonElectronicDocument") + { + assetType = "record-nonelec"; + } + else + { + permissions["download"] = true; + } + + /* Record Type evaluator */ + recordType = Evaluator.recordType(asset); + if (recordType != null) + { + status[recordType] = true; + } + + /* Undeclare Record */ + if (asset.hasAspect("rma:cutOff") == false) + { + permissions["undeclare"] = true; + } + break; + + + /** + * SPECIFIC TO: GHOSTED RECORD FOLDER (Metadata Stub Folder) + */ + case "metadata-stub-folder": + actionSet = "metadataStubFolder"; + + /* Destroyed status */ + status["destroyed"] = true; + break; + + + /** + * SPECIFIC TO: GHOSTED RECORD (Metadata Stub) + */ + case "metadata-stub": + actionSet = "metadataStub"; + + /* Destroyed status */ + status["destroyed"] = true; + + /* Record Type evaluator */ + recordType = Evaluator.recordType(asset); + if (recordType != null) + { + status[recordType] = true; + } + break; + + + /** + * SPECIFIC TO: UNDECLARED RECORD + */ + case "undeclared-record": + actionSet = "undeclaredRecord"; + + /* Electronic/Non-electronic documents */ + if (asset.typeShort == "rma:nonElectronicDocument") + { + assetType = "undeclared-record-nonelec"; + } + else + { + permissions["download"] = true; + + /* Record Type evaluator */ + recordType = Evaluator.recordType(asset); + if (recordType != null) + { + status[recordType] = true; + } + else + { + permissions["set-record-type"] = true; + } + } + break; + + + /** + * SPECIFIC TO: TRANSFER CONTAINERS + */ + case "transfer-container": + actionSet = "transferContainer"; + suppressRoles = true; + break; + + + /** + * SPECIFIC TO: HOLD CONTAINERS + */ + case "hold-container": + actionSet = "holdContainer"; + permissions["Unfreeze"] = true; + permissions["ViewUpdateReasonsForFreeze"] = true; + suppressRoles = true; + break; + + + /** + * SPECIFIC TO: LEGACY TYPES + */ + default: + actionSet = assetType; + break; + } + + return ( + { + assetType: assetType, + actionSet: actionSet, + permissions: permissions, + createdBy: Common.getPerson(asset.properties["cm:creator"]), + modifiedBy: Common.getPerson(asset.properties["cm:modifier"]), + status: status, + metadata: Evaluator.getMetadata(asset, assetType), + suppressRoles: suppressRoles + }); + } +}; diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-filters.lib.js b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-filters.lib.js new file mode 100644 index 0000000000..9949dd5dbc --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-filters.lib.js @@ -0,0 +1,183 @@ +var Filters = +{ + /** + * Type map to filter required types + * NOTE: "documents" filter also returns folders to show UI hint about hidden folders. + */ + TYPE_MAP: + { + "documents": '+(TYPE:"{http://www.alfresco.org/model/content/1.0}content" OR TYPE:"{http://www.alfresco.org/model/application/1.0}filelink" OR TYPE:"{http://www.alfresco.org/model/content/1.0}folder")', + "folders": '+(TYPE:"{http://www.alfresco.org/model/content/1.0}folder" OR TYPE:"{http://www.alfresco.org/model/application/1.0}folderlink")', + "images": "-TYPE:\"{http://www.alfresco.org/model/content/1.0}thumbnail\" +@cm\\:content.mimetype:image/*" + }, + + /** + * Query templates for custom search + */ + QUERY_TEMPLATES: + [ + {field: "keywords", template: "%(cm:name cm:title cm:description TEXT)"}, + {field: "name", template: "%(cm:name)"}, + {field: "title", template: "%(cm:title)"}, + {field: "description", template: "%(cm:description)"}, + {field: "creator", template: "%(cm:creator)"}, + {field: "created", template: "%(cm:created)"}, + {field: "modifier", template: "%(cm:modifier)"}, + {field: "modified", template: "%(cm:modified)"}, + {field: "author", template: "%(cm:author)"}, + {field: "markings", template: "%(rmc:supplementalMarkingList)"}, + {field: "dispositionEvents", template: "%(rma:recordSearchDispositionEvents)"}, + {field: "dispositionActionName", template: "%(rma:recordSearchDispositionActionName)"}, + {field: "dispositionActionAsOf", template: "%(rma:recordSearchDispositionActionAsOf)"}, + {field: "dispositionEventsEligible", template: "%(rma:recordSearchDispositionEventsEligible)"}, + {field: "dispositionPeriod", template: "%(rma:recordSearchDispositionPeriod)"}, + {field: "hasDispositionSchedule", template: "%(rma:recordSearchHasDispositionSchedule)"}, + {field: "dispositionInstructions", template: "%(rma:recordSearchDispositionInstructions)"}, + {field: "dispositionAuthority", template: "%(rma:recordSearchDispositionAuthority)"}, + {field: "holdReason", template: "%(rma:recordSearchHoldReason)"}, + {field: "vitalRecordReviewPeriod", template: "%(rma:recordSearchVitalRecordReviewPeriod)"} + ], + + /** + * Create filter parameters based on input parameters + * + * @method getFilterParams + * @param filter {string} Required filter + * @param parsedArgs {object} Parsed arguments object literal + * @param optional {object} Optional arguments depending on filter type + * @return {object} Object literal containing parameters to be used in Lucene search + */ + getFilterParams: function Filter_getFilterParams(filter, parsedArgs, optional) + { + var filterParams = + { + query: "+PATH:\"" + parsedArgs.pathNode.qnamePath + "/*\"", + limitResults: null, + sort: [ + { + column: "@{http://www.alfresco.org/model/content/1.0}name", + ascending: true + }], + language: "lucene", + templates: null, + variablePath: true + }; + + // Max returned results specified? + var argMax = args.max; + if ((argMax !== null) && !isNaN(argMax)) + { + filterParams.limitResults = argMax; + } + + // Create query based on passed-in arguments + var filterData = args.filterData, + filterQuery = ""; + + // Common types and aspects to filter from the UI + var filterQueryDefaults = " -TYPE:\"{http://www.alfresco.org/model/content/1.0}thumbnail\"" + + " -TYPE:\"{http://www.alfresco.org/model/content/1.0}systemfolder\"" + + " -TYPE:\"{http://www.alfresco.org/model/recordsmanagement/1.0}dispositionSchedule\"" + + " -TYPE:\"{http://www.alfresco.org/model/recordsmanagement/1.0}dispositionActionDefinition\"" + + " -TYPE:\"{http://www.alfresco.org/model/recordsmanagement/1.0}dispositionAction\"" + + " -TYPE:\"{http://www.alfresco.org/model/recordsmanagement/1.0}hold\"" + + " -TYPE:\"{http://www.alfresco.org/model/recordsmanagement/1.0}transfer\""; + + // Create query based on passed-in arguments + switch (String(filter)) + { + case "all": + filterQuery = "+PATH:\"" + parsedArgs.rootNode.qnamePath + "//*\""; + filterQuery += " -TYPE:\"{http://www.alfresco.org/model/content/1.0}folder\""; + filterParams.query = filterQuery + filterQueryDefaults; + break; + + case "node": + parsedArgs.pathNode = parsedArgs.rootNode.parent; + filterParams.variablePath = false; + filterParams.query = "+ID:\"" + parsedArgs.rootNode.nodeRef + "\""; + break; + + case "savedsearch": + var searchNode = parsedArgs.location.siteNode.getContainer("Saved Searches"); + if (searchNode != null) + { + var ssNode = searchNode.childByNamePath(String(filterData)); + + if (ssNode != null) + { + var ssJson = eval('try{(' + ssNode.content + ')}catch(e){}'); + filterQuery = ssJson.query; + // Wrap the query so that only valid items within the filePlan are returned + filterParams.query = 'PATH:"' + parsedArgs.rootNode.qnamePath + '//*" AND (' + filterQuery + ')'; + filterParams.templates = Filters.QUERY_TEMPLATES; + filterParams.language = "fts-alfresco"; + filterParams.namespace = "http://www.alfresco.org/model/recordsmanagement/1.0"; + // gather up the sort by fields + // they are encoded as "property/dir" i.e. "cm:name/asc" + if (ssJson.sort && ssJson.sort.length !== 0) + { + var sortPairs = ssJson.sort.split(","); + var sort = []; + for (var i=0, j; i + node + Document List Component - rm node data webscript + /slingshot/doclib/rm/node/{store_type}/{store_id}/{id} + argument + user + required + internal + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-node.get.js b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-node.get.js new file mode 100644 index 0000000000..9dd3b3b48c --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-node.get.js @@ -0,0 +1,6 @@ + + + + + +model.doclist = getDoclist("all"); diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-node.get.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-node.get.json.ftl new file mode 100644 index 0000000000..81e520bab6 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-node.get.json.ftl @@ -0,0 +1,34 @@ +<#import "item.lib.ftl" as itemLib /> +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "totalRecords": ${doclist.paging.totalRecords?c}, + "startIndex": ${doclist.paging.startIndex?c}, + "metadata": + { + <#if doclist.filePlan??>"filePlan": "${doclist.filePlan.nodeRef}", + "parent": + { + <#if doclist.parent??> + "nodeRef": "${doclist.parent.node.nodeRef}", + "type": "${doclist.parent.type}", + "permissions": + { + "userAccess": + { + <#list doclist.parent.userAccess?keys as perm> + <#if doclist.parent.userAccess[perm]?is_boolean> + "${perm?string}": ${doclist.parent.userAccess[perm]?string}<#if perm_has_next>, + + + } + } + + } + }, + "item": + { + <@itemLib.itemJSON item=doclist.items[0] />, + "dod5015": <#noescape>${doclist.items[0].dod5015} + } +} + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-savedsearches.get.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-savedsearches.get.desc.xml new file mode 100644 index 0000000000..3b23c5431b --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-savedsearches.get.desc.xml @@ -0,0 +1,9 @@ + + doclist + Document List Component - rm saved searches data webscript + /slingshot/doclib/rm/savedsearches/site/{site}?p={public?} + argument + user + required + internal + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-savedsearches.get.js b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-savedsearches.get.js new file mode 100644 index 0000000000..76fa47dc7f --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-savedsearches.get.js @@ -0,0 +1,54 @@ +function main() +{ + var savedSearches = [], + siteId = url.templateArgs.site, + siteNode = siteService.getSite(siteId), + bPublic = args.p; + + if (siteNode === null) + { + status.setCode(status.STATUS_NOT_FOUND, "Site not found: '" + siteId + "'"); + return null; + } + + var searchNode = siteNode.getContainer("Saved Searches"); + if (searchNode != null) + { + var kids, ssNode; + + if (bPublic == null || bPublic == "true") + { + // public searches are in the root of the folder + kids = searchNode.children; + } + else + { + // user specific searches are in a sub-folder of username + var userNode = searchNode.childByNamePath(person.properties.userName); + if (userNode != null) + { + kids = userNode.children; + } + } + + if (kids) + { + for (var i = 0, ii = kids.length; i < ii; i++) + { + ssNode = kids[i]; + if (ssNode.isDocument) + { + savedSearches.push( + { + name: ssNode.name, + description: ssNode.properties.description + }); + } + } + } + } + + model.savedSearches = savedSearches; +} + +main(); \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-savedsearches.get.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-savedsearches.get.json.ftl new file mode 100644 index 0000000000..aab3d3a94f --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-savedsearches.get.json.ftl @@ -0,0 +1,13 @@ +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "items": + [ + <#list savedSearches as s> + { + "name": "${s.name}", + "description": "${s.description!""}" + }<#if s_has_next>, + + ] +} + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-transfer.get.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-transfer.get.desc.xml new file mode 100644 index 0000000000..03c9efc138 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-transfer.get.desc.xml @@ -0,0 +1,9 @@ + + doclist-transfer + Document List Component - rm transfer query data webscript + /slingshot/doclib/rm/transfer/node/{store_type}/{store_id}/{id} + argument + user + required + internal + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-transfer.get.js b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-transfer.get.js new file mode 100644 index 0000000000..a611074cf2 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-transfer.get.js @@ -0,0 +1,21 @@ +function main() +{ + var nodeRef = url.templateArgs.store_type + "://" + url.templateArgs.store_id + "/" + url.templateArgs.id, + transfer = search.findNode(nodeRef); + + if (transfer === null) + { + status.setCode(status.STATUS_NOT_FOUND, "Not a valid nodeRef: '" + nodeRef + "'"); + return null; + } + + if (String(transfer.typeShort) != "rma:transfer") + { + status.setCode(status.STATUS_BAD_REQUEST, "nodeRef: '" + nodeRef + "' is not of type 'rma:transfer'"); + return null; + } + + model.transfer = transfer; +} + +main(); \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-transfer.get.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-transfer.get.json.ftl new file mode 100644 index 0000000000..ff5ed41e60 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-transfer.get.json.ftl @@ -0,0 +1,13 @@ +<#escape x as jsonUtils.encodeJSONString(x)> +{ + <#if transfer??> + "transfer": + { + "nodeRef": "${transfer.nodeRef}", + "name": "${transfer.name}", + "rma:transferAccessionIndicator": ${(transfer.properties["rma:transferAccessionIndicator"]!false)?string}, + "rma:transferPDFIndicator": ${(transfer.properties["rma:transferPDFIndicator"]!false)?string} + } + +} + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-treenode.get.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-treenode.get.desc.xml new file mode 100644 index 0000000000..e7345e5692 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-treenode.get.desc.xml @@ -0,0 +1,12 @@ + + treenode + Document List Component - rm treenode data webscript + /slingshot/doclib/rm/treenode/site/{site}/{container}/{path} + /slingshot/doclib/rm/treenode/site/{site}/{container} + /slingshot/doclib/rm/treenode/node/{store_type}/{store_id}/{id}/{path} + /slingshot/doclib/rm/treenode/node/{store_type}/{store_id}/{id} + argument + user + required + internal + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-treenode.get.js b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-treenode.get.js new file mode 100644 index 0000000000..ebabc06382 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-treenode.get.js @@ -0,0 +1,130 @@ + + +/** + * Document List Component: treenode + */ +model.treenode = getTreenode(); + +/* Create collection of folders in the given space */ +function getTreenode() +{ + try + { + var items = new Array(), + hasSubfolders = true, + ignoredTypes = + { + "{http://www.alfresco.org/model/forum/1.0}forum": true, + "{http://www.alfresco.org/model/forum/1.0}topic": true, + "{http://www.alfresco.org/model/content/1.0}systemfolder": true + }, + skipPermissionCheck = args["perms"] == "false", + evalChildFolders = args["children"] !== "false", + item, rmNode, capabilities, cap; + + // Use helper function to get the arguments + var parsedArgs = ParseArgs.getParsedArgs(); + if (parsedArgs === null) + { + return; + } + + // Quick version if "skipPermissionCheck" flag set + if (skipPermissionCheck) + { + for each (item in parsedArgs.pathNode.children) + { + if (itemIsAllowed(item) && !(item.type in ignoredTypes)) + { + if (evalChildFolders) + { + hasSubfolders = item.childFileFolders(false, true, "fm:forum").length > 0; + } + + items.push( + { + node: item, + hasSubfolders: hasSubfolders + }); + } + } + } + else + { + for each (item in parsedArgs.pathNode.children) + { + if (itemIsAllowed(item) && !(item.type in ignoredTypes)) + { + capabilities = {}; + rmNode = rmService.getRecordsManagementNode(item); + for each (cap in rmNode.capabilitiesSet("Create")) + { + capabilities[cap.name] = true; + } + + if (evalChildFolders) + { + hasSubfolders = item.childFileFolders(false, true, "fm:forum").length > 0; + } + + items.push( + { + node: item, + hasSubfolders: hasSubfolders, + permissions: + { + create: capabilities["Create"] + } + }); + } + } + } + + items.sort(sortByName); + + return ( + { + parent: parsedArgs.pathNode, + resultsTrimmed: false, + items: items + }); + } + catch(e) + { + status.setCode(status.STATUS_INTERNAL_SERVER_ERROR, e.toString()); + return; + } +} + + +/* Sort the results by case-insensitive name */ +function sortByName(a, b) +{ + return (b.node.name.toLowerCase() > a.node.name.toLowerCase() ? -1 : 1); +} + +/* Filter allowed types, etc. */ +function itemIsAllowed(item) +{ + // Must be a subtype of cm:folder + if (!item.isSubType("cm:folder")) + { + return false; + } + + var typeShort = String(item.typeShort); + + // Don't show Hold and Transfer top-level containers + if (typeShort == "rma:hold" || typeShort == "rma:transfer") + { + return false; + } + + // Must be a "dod:" or "rma:" namespaced type + if (typeShort.indexOf("dod:") !== 0 && typeShort.indexOf("rma") !== 0) + { + return false; + } + + return true; +} \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-treenode.get.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-treenode.get.json.ftl new file mode 100644 index 0000000000..e238c2279d --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary/rm-treenode.get.json.ftl @@ -0,0 +1,39 @@ +<#assign p = treenode.parent> +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "totalResults": ${treenode.items?size?c}, + "resultsTrimmed": ${treenode.resultsTrimmed?string}, + "parent": + { + "nodeRef": "${p.nodeRef}", + "userAccess": + { + "create": ${p.hasPermission("CreateChildren")?string}, + "edit": ${p.hasPermission("Write")?string}, + "delete": ${p.hasPermission("Delete")?string} + } + }, + "items": + [ + <#list treenode.items as item> + <#assign t = item.node> + { + <#if item.permissions??> + "userAccess": + { + <#list item.permissions?keys as perm> + <#if item.permissions[perm]?is_boolean> + "${perm?string}": ${item.permissions[perm]?string}<#if perm_has_next>, + + + }, + + "nodeRef": "${t.nodeRef}", + "name": "${t.name}", + "description": "${(t.properties.description!"")}", + "hasChildren": ${item.hasSubfolders?string} + }<#if item_has_next>, + + ] +} + diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/rmsearch/rmsavedsearches.delete.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/rmsearch/rmsavedsearches.delete.desc.xml new file mode 100644 index 0000000000..ea338656dc --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/rmsearch/rmsavedsearches.delete.desc.xml @@ -0,0 +1,9 @@ + + rmsavedsearches + RM Saved Searches + /slingshot/rmsavedsearches/site/{site}/{name} + argument + user + required + internal + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/rmsearch/rmsavedsearches.delete.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/rmsearch/rmsavedsearches.delete.json.ftl new file mode 100644 index 0000000000..576619debc --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/rmsearch/rmsavedsearches.delete.json.ftl @@ -0,0 +1,3 @@ +{ + "success": ${success?string} +} \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/rmsearch/rmsavedsearches.get.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/rmsearch/rmsavedsearches.get.desc.xml new file mode 100644 index 0000000000..f7824c206b --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/rmsearch/rmsavedsearches.get.desc.xml @@ -0,0 +1,9 @@ + + rmsavedsearches + RM Saved Searches + /slingshot/rmsavedsearches/site/{site}?p={public?} + argument + user + required + internal + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/rmsearch/rmsavedsearches.get.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/rmsearch/rmsavedsearches.get.json.ftl new file mode 100644 index 0000000000..e554900d31 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/rmsearch/rmsavedsearches.get.json.ftl @@ -0,0 +1,16 @@ +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "items": + [ + <#list savedSearches as s> + { + "name": "${s.name}", + "description": "${s.description!""}", + "query": "${s.query}", + "params": "${s.params}", + "sort": "${s.sort}" + }<#if s_has_next>, + + ] +} + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/rmsearch/rmsavedsearches.post.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/rmsearch/rmsavedsearches.post.desc.xml new file mode 100644 index 0000000000..dd07b5143c --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/rmsearch/rmsavedsearches.post.desc.xml @@ -0,0 +1,9 @@ + + rmsavedsearches + RM Saved Searches + /slingshot/rmsavedsearches/site/{site} + argument + user + required + internal + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/rmsearch/rmsavedsearches.post.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/rmsearch/rmsavedsearches.post.json.ftl new file mode 100644 index 0000000000..576619debc --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/rmsearch/rmsavedsearches.post.json.ftl @@ -0,0 +1,3 @@ +{ + "success": ${success?string} +} \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/rmsearch/rmsearch.get.desc.xml b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/rmsearch/rmsearch.get.desc.xml new file mode 100644 index 0000000000..c97310a712 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/rmsearch/rmsearch.get.desc.xml @@ -0,0 +1,9 @@ + + rmsearch + Record Search Component Data Webscript + /slingshot/rmsearch/{site}?query={query?}&sortby={sortby?}&filters={filters?}&maxitems={maxitems?} + argument + user + required + internal + \ No newline at end of file diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/rmsearch/rmsearch.get.json.ftl b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/rmsearch/rmsearch.get.json.ftl new file mode 100644 index 0000000000..2f64c6f7a3 --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/rmsearch/rmsearch.get.json.ftl @@ -0,0 +1,42 @@ +<#escape x as jsonUtils.encodeJSONString(x)> +{ + "items": + [ + <#list items as item> + { + "nodeRef": "${item.nodeRef}", + "type": "${item.type}", + "name": "${item.name}", + "title": "${item.title!''}", + "description": "${item.description!''}", + "modifiedOn": "${xmldate(item.modifiedOn)}", + "modifiedByUser": "${item.modifiedByUser}", + "modifiedBy": "${item.modifiedBy}", + "createdOn": "${xmldate(item.createdOn)}", + "createdByUser": "${item.createdByUser}", + "createdBy": "${item.createdBy}", + "author": "${item.author!''}", + "size": ${item.size?c}, + <#if item.browseUrl??>"browseUrl": "${item.browseUrl}", + "parentFolder": "${item.parentFolder!""}", + "properties": + { + <#assign first=true> + <#list item.properties?keys as k> + <#if item.properties[k]??> + <#if !first>,<#else><#assign first=false>"${k}": + <#assign prop = item.properties[k]> + <#if prop?is_date>"${xmldate(prop)}" + <#elseif prop?is_boolean>${prop?string("true", "false")} + <#elseif prop?is_enumerable>[<#list prop as p>"${p}"<#if p_has_next>, ] + <#elseif prop?is_number>${prop?c} + <#else>"${prop}" + + + + } + }<#if item_has_next>, + + ] +} + \ No newline at end of file diff --git a/rm-server/gradle.properties b/rm-server/gradle.properties new file mode 100644 index 0000000000..16e6a91c08 --- /dev/null +++ b/rm-server/gradle.properties @@ -0,0 +1,2 @@ +warFile=deps/alfresco.war +warFileName=Alfresco diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/FilePlanComponentKind.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/FilePlanComponentKind.java new file mode 100644 index 0000000000..3f7da216df --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/FilePlanComponentKind.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.module.org_alfresco_module_rm; + +/** + * File plan component kind enumeration class. + *
+ * Helpful when trying to determine the characteristics of a kind + * of file plan component. + * + * @author Roy Wetherall + */ +public enum FilePlanComponentKind +{ + FILE_PLAN_COMPONENT, + FILE_PLAN, + RECORD_CATEGORY, + RECORD_FOLDER, + RECORD, + TRANSFER, + HOLD, + DISPOSITION_SCHEDULE; +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/RecordsManagementAdminService.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/RecordsManagementAdminService.java new file mode 100644 index 0000000000..cc33374030 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/RecordsManagementAdminService.java @@ -0,0 +1,388 @@ +/* + * 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.module.org_alfresco_module_rm; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.module.org_alfresco_module_rm.caveat.RMListOfValuesConstraint.MatchLogic; +import org.alfresco.service.cmr.dictionary.AssociationDefinition; +import org.alfresco.service.cmr.dictionary.ConstraintDefinition; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + +/** + * Records management custom model service interface. Implementations of this class are responsible + * for the creation and maintenance of RM-related custom properties and custom associations. + * + * @author Neil McErlean, janv + */ +public interface RecordsManagementAdminService +{ + /** + * Initialise the custom model + */ + public void initialiseCustomModel(); + + /** + * Get a list of all registered customisable types and aspects. + * + * @return {@link Set}<{@link QName}> QName's of customisable types and aspects + */ + public Set getCustomisable(); + + /** + * Get a list of all the registered customisable types and aspects present on a given + * node reference. + * + * @param nodeRef node reference + * @return {@link Set}<{@link QName}> QName's of customisable types and aspects, empty if none + */ + public Set getCustomisable(NodeRef nodeRef); + + /** + * Indicates whether a type (or aspect) is customisable. + * + * @param type customisable type {@link QName} + * @return boolean true if type customisable, false otherwise + */ + public boolean isCustomisable(QName type); + + /** + * Makes a type customisable. + * + * @param type type {@link QName} to make customisable + */ + public void makeCustomisable(QName type); + + /** + * Assuming the custom properties are not in use, makes a type no longer customisable. + * + * @param type type {@link QName} to make customisable + */ + public void unmakeCustomisable(QName type); + + /** + * Indicates whether the custom property exists. + * + * @param property properties {@link QName} + * @return boolean true if property exists, false otherwise + */ + public boolean existsCustomProperty(QName property); + + /** + * This method returns the custom properties that have been defined for the specified + * customisable RM element. + *

+ * Note: the custom property definitions are retrieved from the dictionaryService + * which is notified of any newly created definitions on transaction commit. + * Therefore custom properties created in the current transaction will not appear + * in the result of this method. + * + * @param customisedElement + * @return + * @see CustomisableRmElement + */ + public Map getCustomPropertyDefinitions(QName customisableType); + + /** + * This method returns the custom properties that have been defined for all of + * the specified customisable RM elements. + * Note: the custom property definitions are retrieved from the dictionaryService + * which is notified of any newly created definitions on transaction commit. + * Therefore custom properties created in the current transaction will not appear + * in the result of this method. + * + * @return + * @see CustomisableRmElement + */ + public Map getCustomPropertyDefinitions(); + + /** + * Add custom property definition + * + * Note: no default value, single valued, optional, not system protected, no constraints + * + * @param propId - If a value for propId is provided it will be used to identify property definitions + * within URLs and in QNames. Therefore it must contain URL/QName-valid characters + * only. It must also be unique. + * If a null value is passed, an id will be generated. + * @param aspectName - mandatory. The aspect within which the property is to be defined. + * This must be one of the CustomisableRmElements. + * @param label - mandatory + * @param dataType - mandatory + * @param title - optional + * @param description - optional + * + * @return the propId, whether supplied as a parameter or generated. + * @see CustomisableRmElement#getCorrespondingAspect() + */ + public QName addCustomPropertyDefinition(QName propId, QName typeName, String label, QName dataType, String title, String description); + + /** + * Add custom property definition with one optional constraint reference + * + * @param propId - If a value for propId is provided it will be used to identify property definitions + * within URLs and in QNames. Therefore it must contain URL/QName-valid characters + * only. It must also be unique. + * If a null value is passed, an id will be generated. + * @param aspectName - mandatory. The aspect within which the property is to be defined. + * This must be one of the CustomisableRmElements. + * @param label - mandatory + * @param dataType - mandatory + * @param title - optional + * @param description - optional + * @param defaultValue - optional + * @param multiValued - TRUE if multi-valued property + * @param mandatory - TRUE if mandatory property + * @param isProtected - TRUE if protected property + * @param lovConstraintQName - optional custom constraint + * + * @return the propId, whether supplied as a parameter or generated. + * @see CustomisableRmElement#getCorrespondingAspect() + */ + + // TODO propId string (not QName) ? + // TODO remove title (since it is ignored) (or remove label to title) + + public QName addCustomPropertyDefinition(QName propId, QName typeName, String label, QName dataType, String title, String description, String defaultValue, boolean multiValued, boolean mandatory, boolean isProtected, QName lovConstraintQName); + + /** + * Update the custom property definition's label (title). + * + * @param propQName the qname of the property definition + * @param newLabel the new value for the label. + * @return the propId. + */ + public QName setCustomPropertyDefinitionLabel(QName propQName, String newLabel); + + /** + * Sets a new list of values constraint on the custom property definition. + * + * @param propQName the qname of the property definition + * @param newLovConstraint the List-Of-Values constraintRef. + * @return the propId. + */ + public QName setCustomPropertyDefinitionConstraint(QName propQName, QName newLovConstraint); + + /** + * Removes all list of values constraints from the custom property definition. + * + * @param propQName the qname of the property definition + * @return the propId. + */ + public QName removeCustomPropertyDefinitionConstraints(QName propQName); + + /** + * Remove custom property definition + * + * @param propQName + */ + public void removeCustomPropertyDefinition(QName propQName); + + /** + * This method returns the custom references that have been defined in the custom + * model. + * Note: the custom reference definitions are retrieved from the dictionaryService + * which is notified of any newly created definitions on transaction commit. + * Therefore custom references created in the current transaction will not appear + * in the results. + * + * @return The Map of custom references (both parent-child and standard). + */ + public Map getCustomReferenceDefinitions(); + + /** + * Fetches all associations from the given source. + * + * @param node the node from which the associations start. + * @return a List of associations. + */ + public List getCustomReferencesFrom(NodeRef node); + + /** + * Fetches all child associations of the given source. i.e. all associations where the + * given node is the parent. + * + * @param node + * @return + */ + public List getCustomChildReferences(NodeRef node); + + /** + * Returns a List of all associations to the given node. + * + * @param node the node to which the associations point. + * @return a List of associations. + */ + public List getCustomReferencesTo(NodeRef node); + + /** + * Fetches all child associations where the given node is the child. + * + * @param node + * @return + */ + public List getCustomParentReferences(NodeRef node); + + /** + * This method adds the specified custom reference instance between the specified nodes. + * Only one instance of any custom reference type is allowed in a given direction + * between two given records. + * + * @param fromNode + * @param toNode + * @param assocId the server-side qname e.g. {http://www.alfresco.org/model/rmcustom/1.0}abcd-12-efgh-4567 + * @throws AlfrescoRuntimeException if an instance of the specified reference type + * already exists from fromNode to toNode. + */ + public void addCustomReference(NodeRef fromNode, NodeRef toNode, QName assocId); + + /** + * This method removes the specified custom reference instance from the specified node. + * + * @param fromNode + * @param toNode + * @param assocId the server-side qname e.g. {http://www.alfresco.org/model/rmcustom/1.0}abcd-12-efgh-4567 + */ + public void removeCustomReference(NodeRef fromNode, NodeRef toNode, QName assocId); + + /** + * This method creates a new custom association, using the given label as the title. + * + * @param label the title of the association definition + * @return the QName of the newly-created association. + */ + public QName addCustomAssocDefinition(String label); + + /** + * This method creates a new custom child association, combining the given source and + * target and using the combined String as the title. + * + * @param source + * @param target + * @return the QName of the newly-created association. + */ + public QName addCustomChildAssocDefinition(String source, String target); + + /** + * This method updates the source and target values for the specified child association. + * The source and target will be combined into a single string and stored in the title property. + * Source and target are String metadata for RM parent/child custom references. + * + * @param refQName qname of the child association. + * @param newSource the new value for the source field. + * @param newTarget the new value for the target field. + * @see #getCompoundIdFor(String, String) + * @see #splitSourceTargetId(String) + */ + public QName updateCustomChildAssocDefinition(QName refQName, String newSource, String newTarget); + + /** + * This method updates the label value for the specified association. + * The label will be stored in the title property. + * Label is String metadata for bidirectional custom references. + * + * @param refQName qname of the child association. + * @param newLabel the new value for the label field. + */ + public QName updateCustomAssocDefinition(QName refQName, String newLabel); + + /** + * This method returns ConstraintDefinition objects defined in the given model + * (note: not property references or in-line defs) + * The custom constraint definitions are retrieved from the dictionaryService + * which is notified of any newly created definitions on transaction commit. + * Therefore custom constraints created in the current transaction will not appear + * in the results. + */ + public List getCustomConstraintDefinitions(QName modelQName); + + /** + * This method adds a Constraint definition to the custom model. + * The implementation of this method would have to go into the M2Model and insert + * the relevant M2Objects for this new constraint. + * + * param type not included as it would always be RMListOfValuesConstraint for RM. + * + * @param constraintName the name e.g. rmc:foo + * @param title the human-readable title e.g. My foo list + * @param caseSensitive + * @param allowedValues the allowed values list + * @param matchLogic AND (all values must match), OR (at least one values must match) + */ + public void addCustomConstraintDefinition(QName constraintName, String title, boolean caseSensitive, List allowedValues, MatchLogic matchLogic); + + /** + * Remove custom constraint definition - if not referenced (by any properties) + * + * + * @param constraintName the name e.g. rmc:foo + */ + public void removeCustomConstraintDefinition(QName constraintName); + + /** + * Update custom constraint definition with new list of values (replaces existing list, if any) + * + * @param constraintName the name e.g. rmc:foo + * @param newValues + */ + public void changeCustomConstraintValues(QName constraintName, List newValues); + + /** + * + * @param constraintName + * @param title + */ + public void changeCustomConstraintTitle(QName constraintName, String title); + + /** + * This method iterates over the custom properties, references looking for one whose id + * exactly matches that specified. + * + * @param localName the localName part of the qname of the property or reference definition. + * @return the QName of the property, association definition which matches, or null. + */ + public QName getQNameForClientId(String localName); + + /** + * Given a compound id for source and target strings (as used with parent/child + * custom references), this method splits the String and returns an array containing + * the source and target IDs separately. + * + * @param sourceTargetId the compound ID. + * @return a String array, where result[0] == sourceId and result[1] == targetId. + */ + public String[] splitSourceTargetId(String sourceTargetId); + + /** + * This method retrieves a compound ID (client-side) for the specified + * sourceId and targetId. + * + * @param sourceId + * @param targetId + * @return + */ + public String getCompoundIdFor(String sourceId, String targetId); +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/RecordsManagementAdminServiceImpl.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/RecordsManagementAdminServiceImpl.java new file mode 100644 index 0000000000..3500f1d704 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/RecordsManagementAdminServiceImpl.java @@ -0,0 +1,1540 @@ +/* + * 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.module.org_alfresco_module_rm; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementPolicies.BeforeCreateReference; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementPolicies.BeforeRemoveReference; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementPolicies.OnCreateReference; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementPolicies.OnRemoveReference; +import org.alfresco.module.org_alfresco_module_rm.caveat.RMListOfValuesConstraint; +import org.alfresco.module.org_alfresco_module_rm.caveat.RMListOfValuesConstraint.MatchLogic; +import org.alfresco.module.org_alfresco_module_rm.dod5015.DOD5015Model; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementCustomModel; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.dictionary.DictionaryRepositoryBootstrap; +import org.alfresco.repo.dictionary.IndexTokenisationMode; +import org.alfresco.repo.dictionary.M2Aspect; +import org.alfresco.repo.dictionary.M2Association; +import org.alfresco.repo.dictionary.M2ChildAssociation; +import org.alfresco.repo.dictionary.M2ClassAssociation; +import org.alfresco.repo.dictionary.M2Constraint; +import org.alfresco.repo.dictionary.M2Model; +import org.alfresco.repo.dictionary.M2Namespace; +import org.alfresco.repo.dictionary.M2Property; +import org.alfresco.repo.node.NodeServicePolicies; +import org.alfresco.repo.policy.ClassPolicyDelegate; +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.policy.Behaviour.NotificationFrequency; +import org.alfresco.service.cmr.dictionary.AspectDefinition; +import org.alfresco.service.cmr.dictionary.AssociationDefinition; +import org.alfresco.service.cmr.dictionary.Constraint; +import org.alfresco.service.cmr.dictionary.ConstraintDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryException; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.dictionary.TypeDefinition; +import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +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; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.alfresco.util.GUID; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.surf.util.I18NUtil; +import org.springframework.extensions.surf.util.ParameterCheck; + +/** + * Records Management AdminService Implementation. + * + * @author Neil McErlean, janv + */ +public class RecordsManagementAdminServiceImpl implements RecordsManagementAdminService, + RecordsManagementCustomModel, + NodeServicePolicies.OnAddAspectPolicy, + NodeServicePolicies.OnRemoveAspectPolicy, + NodeServicePolicies.OnCreateNodePolicy +{ + /** Logger */ + private static Log logger = LogFactory.getLog(RecordsManagementAdminServiceImpl.class); + + /** I18N messages*/ + private static final String MSG_SERVICE_NOT_INIT = "rm.admin.service-not-init"; + private static final String MSG_NOT_CUSTOMISABLE = "rm.admin.not-customisable"; + private static final String MSG_INVALID_CUSTOM_ASPECT = "rm.admin.invalid-custom-aspect"; + private static final String MSG_PROPERTY_ALREADY_EXISTS = "rm.admin.property-already-exists"; + private static final String MSG_CANNOT_APPLY_CONSTRAINT = "rm.admin.cannot-apply-constraint"; + private static final String MSG_PROP_EXIST = "rm.admin.prop-exist"; + private static final String MSG_CUSTOM_PROP_EXIST = "rm.admin.custom-prop-exist"; + private static final String MSG_UNKNOWN_ASPECT = "rm.admin.unknown-aspect"; + private static final String MSG_REF_EXIST = "rm.admin.ref-exist"; + private static final String MSG_REF_LABEL_IN_USE = "rm.admin.ref-label-in-use"; + private static final String MSG_ASSOC_EXISTS = "rm.admin.assoc-exists"; + private static final String MSG_CHILD_ASSOC_EXISTS = "rm.admin.child-assoc-exists"; + private static final String MSG_CONNOT_FIND_ASSOC_DEF = "rm.admin.cannot-find-assoc-def"; + private static final String MSG_CONSTRAINT_EXISTS = "rm.admin.constraint-exists"; + private static final String MSG_CANNOT_FIND_CONSTRAINT = "rm.admin.contraint-cannot-find"; + private static final String MSG_UNEXPECTED_TYPE_CONSTRAINT = "rm.admin.unexpected_type_constraint"; + private static final String MSG_CUSTOM_MODEL_NOT_FOUND = "rm.admin.custom-model-not-found"; + private static final String MSG_CUSTOM_MODEL_NO_CONTENT = "rm.admin.custom-model-no-content"; + private static final String MSG_ERROR_WRITE_CUSTOM_MODEL = "rm.admin.error-write-custom-model"; + private static final String MSG_ERROR_CLIENT_ID = "rm.admin.error-client-id"; + private static final String MSG_ERROR_SPLIT_ID = "rm.admin.error-split-id"; + + /** Constants */ + public static final String RMC_CUSTOM_ASSOCS = RecordsManagementCustomModel.RM_CUSTOM_PREFIX + ":customAssocs"; + private static final String CUSTOM_CONSTRAINT_TYPE = org.alfresco.module.org_alfresco_module_rm.caveat.RMListOfValuesConstraint.class.getName(); + private static final NodeRef RM_CUSTOM_MODEL_NODE_REF = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, "records_management_custom_model"); + private static final String PARAM_ALLOWED_VALUES = "allowedValues"; + private static final String PARAM_CASE_SENSITIVE = "caseSensitive"; + private static final String PARAM_MATCH_LOGIC = "matchLogic"; + public static final String RMA_RECORD = "rma:record"; + private static final String SOURCE_TARGET_ID_SEPARATOR = "__"; + + /** Dictionary service */ + private DictionaryService dictionaryService; + + /** Namespace service */ + private NamespaceService namespaceService; + + /** Node service */ + private NodeService nodeService; + + /** Content service */ + private ContentService contentService; + + /** Dictionary repository bootstrap */ + private DictionaryRepositoryBootstrap dictonaryRepositoryBootstrap; + + /** Policy component */ + private PolicyComponent policyComponent; + + /** Policy delegates */ + private ClassPolicyDelegate beforeCreateReferenceDelegate; + private ClassPolicyDelegate onCreateReferenceDelegate; + private ClassPolicyDelegate beforeRemoveReferenceDelegate; + private ClassPolicyDelegate onRemoveReferenceDelegate; + + /** List of types that can be customisable */ + private List pendingCustomisableTypes; + private Map customisableTypes; + + /** + * @param dictionaryService the dictionary service + */ + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + /** + * @param namespaceService the namespace service + */ + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + /** + * @param nodeService the node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * @param contentService the content service + */ + public void setContentService(ContentService contentService) + { + this.contentService = contentService; + } + + /** + * @param policyComponent the policy component + */ + public void setPolicyComponent(PolicyComponent policyComponent) + { + this.policyComponent = policyComponent; + } + + /** + * Bootstrap for standard (non-RMC) dynamic models + * + * @param dictonaryRepositoryBootstrap dictionary repository bootstrap + */ + public void setDictionaryRepositoryBootstrap(DictionaryRepositoryBootstrap dictonaryRepositoryBootstrap) + { + this.dictonaryRepositoryBootstrap = dictonaryRepositoryBootstrap; + } + + /** + * Initialisation method + */ + public void init() + { + // Register the various policies + beforeCreateReferenceDelegate = policyComponent.registerClassPolicy(BeforeCreateReference.class); + onCreateReferenceDelegate = policyComponent.registerClassPolicy(OnCreateReference.class); + beforeRemoveReferenceDelegate = policyComponent.registerClassPolicy(BeforeRemoveReference.class); + onRemoveReferenceDelegate = policyComponent.registerClassPolicy(OnRemoveReference.class); + } + + protected void invokeBeforeCreateReference(NodeRef fromNodeRef, NodeRef toNodeRef, QName reference) + { + // get qnames to invoke against + Set qnames = RecordsManagementPoliciesUtil.getTypeAndAspectQNames(nodeService, fromNodeRef); + // execute policy for node type and aspects + BeforeCreateReference policy = beforeCreateReferenceDelegate.get(qnames); + policy.beforeCreateReference(fromNodeRef, toNodeRef, reference); + } + + protected void invokeOnCreateReference(NodeRef fromNodeRef, NodeRef toNodeRef, QName reference) + { + // get qnames to invoke against + Set qnames = RecordsManagementPoliciesUtil.getTypeAndAspectQNames(nodeService, fromNodeRef); + // execute policy for node type and aspects + OnCreateReference policy = onCreateReferenceDelegate.get(qnames); + policy.onCreateReference(fromNodeRef, toNodeRef, reference); + } + + protected void invokeBeforeRemoveReference(NodeRef fromNodeRef, NodeRef toNodeRef, QName reference) + { + // get qnames to invoke against + Set qnames = RecordsManagementPoliciesUtil.getTypeAndAspectQNames(nodeService, fromNodeRef); + // execute policy for node type and aspects + BeforeRemoveReference policy = beforeRemoveReferenceDelegate.get(qnames); + policy.beforeRemoveReference(fromNodeRef, toNodeRef, reference); + } + + protected void invokeOnRemoveReference(NodeRef fromNodeRef, NodeRef toNodeRef, QName reference) + { + // get qnames to invoke against + Set qnames = RecordsManagementPoliciesUtil.getTypeAndAspectQNames(nodeService, fromNodeRef); + // execute policy for node type and aspects + OnRemoveReference policy = onRemoveReferenceDelegate.get(qnames); + policy.onRemoveReference(fromNodeRef, toNodeRef, reference); + } + + @Override + public void onAddAspect(NodeRef nodeRef, QName aspectTypeQName) + { + if (nodeService.exists(nodeRef) == true && + isCustomisable(aspectTypeQName) == true) + { + QName customPropertyAspect = getCustomAspect(aspectTypeQName); + nodeService.addAspect(nodeRef, customPropertyAspect, null); + } + } + + @Override + public void onRemoveAspect(NodeRef nodeRef, QName aspectTypeQName) + { + if (nodeService.exists(nodeRef) == true && + isCustomisable(aspectTypeQName) == true) + { + QName customPropertyAspect = getCustomAspect(aspectTypeQName); + nodeService.removeAspect(nodeRef, customPropertyAspect); + } + } + + @Override + public void onCreateNode(ChildAssociationRef childAssocRef) + { + NodeRef nodeRef = childAssocRef.getChildRef(); + QName type = nodeService.getType(nodeRef); + while (type != null && ContentModel.TYPE_CMOBJECT.equals(type) == false) + { + if (isCustomisable(type) == true) + { + QName customPropertyAspect = getCustomAspect(type); + nodeService.addAspect(nodeRef, customPropertyAspect, null); + } + + TypeDefinition def = dictionaryService.getType(type); + if (def != null) + { + type = def.getParentName(); + } + else + { + type = null; + } + } + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminService#initialiseCustomModel() + */ + public void initialiseCustomModel() + { + // Bind class behaviours + policyComponent.bindClassBehaviour( + NodeServicePolicies.OnAddAspectPolicy.QNAME, + this, + new JavaBehaviour(this, "onAddAspect", NotificationFrequency.FIRST_EVENT)); + policyComponent.bindClassBehaviour( + NodeServicePolicies.OnRemoveAspectPolicy.QNAME, + this, + new JavaBehaviour(this, "onRemoveAspect", NotificationFrequency.FIRST_EVENT)); + policyComponent.bindClassBehaviour( + NodeServicePolicies.OnCreateNodePolicy.QNAME, + this, + new JavaBehaviour(this, "onCreateNode", NotificationFrequency.FIRST_EVENT)); + + // Initialise the map + getCustomisableMap(); + } + + /** + * @param customisableTypes list of string representations of the type qnames that are customisable + */ + public void setCustomisableTypes(List customisableTypes) + { + pendingCustomisableTypes = new ArrayList(); + for (String customisableType : customisableTypes) + { + pendingCustomisableTypes.add(QName.createQName(customisableType, namespaceService)); + } + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminService#getCustomisable() + */ + public Set getCustomisable() + { + return getCustomisableMap().keySet(); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminService#getCustomisable(org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + public Set getCustomisable(NodeRef nodeRef) + { + Set result = new HashSet(5); + + // Check the nodes hierarchy for customisable types + QName type = nodeService.getType(nodeRef); + while (type != null && ContentModel.TYPE_CMOBJECT.equals(type) == false) + { + // Add to the list if the type is customisable + if (isCustomisable(type) == true) + { + result.add(type); + } + + // Type and get the types parent + TypeDefinition def = dictionaryService.getType(type); + if (def != null) + { + type = def.getParentName(); + } + else + { + type = null; + } + } + + // Get all the nodes aspects + Set aspects = nodeService.getAspects(nodeRef); + for (QName aspect : aspects) + { + QName tempAspect = QName.createQName(aspect.toString()); + while (tempAspect != null) + { + // Add to the list if the aspect is customisable + if (isCustomisable(tempAspect) == true) + { + result.add(tempAspect); + } + + // Try and get the parent aspect + AspectDefinition aspectDef = dictionaryService.getAspect(tempAspect); + if (aspectDef != null) + { + tempAspect = aspectDef.getParentName(); + } + else + { + tempAspect = null; + } + } + } + + return result; + } + + /** + * Gets a map containing all the customisable types + * + * @return map from the customisable type to its custom aspect + */ + private Map getCustomisableMap() + { + if (customisableTypes == null) + { + customisableTypes = new HashMap(7); + Collection aspects = dictionaryService.getAspects(RM_CUSTOM_MODEL); + for (QName aspect : aspects) + { + AspectDefinition aspectDef = dictionaryService.getAspect(aspect); + String name = aspectDef.getName().getLocalName(); + if (name.endsWith("Properties") == true) + { + QName type = null; + String prefixString = aspectDef.getDescription(); + if (prefixString == null) + { + // Backward compatibility from previous RM V1.0 custom models + if ("customRecordProperties".equals(name) == true) + { + type = RecordsManagementModel.ASPECT_RECORD; + } + else if ("customRecordFolderProperties".equals(name) == true) + { + type = RecordsManagementModel.TYPE_RECORD_FOLDER; + } + else if ("customRecordCategoryProperties".equals(name) == true) + { + type = RecordsManagementModel.TYPE_RECORD_CATEGORY; + } + else if ("customRecordSeriesProperties".equals(name) == true) + { + // Only add the deprecated record series type as customisable if + // a v1.0 installation has added custom properties + if (aspectDef.getProperties().size() != 0) + { + type = DOD5015Model.TYPE_RECORD_SERIES; + } + } + } + else + { + type = QName.createQName(prefixString, namespaceService); + } + + // Add the customisable type to the map + if (type != null) + { + customisableTypes.put(type, aspect); + + // Remove customisable type from the pending list + if (pendingCustomisableTypes != null && pendingCustomisableTypes.contains(type) == true) + { + pendingCustomisableTypes.remove(type); + } + } + } + } + + // Deal with any pending types left over + if (pendingCustomisableTypes != null && pendingCustomisableTypes.size() != 0) + { + NodeRef modelRef = getCustomModelRef(RecordsManagementModel.RM_CUSTOM_URI); + M2Model model = readCustomContentModel(modelRef); + try + { + for (QName customisableType : pendingCustomisableTypes) + { + QName customAspect = getCustomAspectImpl(customisableType); + + // Create the new aspect to hold the custom properties + M2Aspect aspect = model.createAspect(customAspect.toPrefixString(namespaceService)); + aspect.setDescription(customisableType.toPrefixString(namespaceService)); + + // Make a record of the customisable type + customisableTypes.put(customisableType, customAspect); + } + } + finally + { + writeCustomContentModel(modelRef, model); + } + } + } + return customisableTypes; + } + + /** + * Gets the QName of the custom aspect given the customisable type QName + * + * @param customisableType + * @return + */ + private QName getCustomAspect(QName customisableType) + { + Map map = getCustomisableMap(); + QName result = map.get(customisableType); + if (result == null) + { + result = getCustomAspectImpl(customisableType); + } + return result; + } + + /** + * Builds a custom aspect QName from a customisable type/aspect QName + * + * @param customisableType + * @return + */ + private QName getCustomAspectImpl(QName customisableType) + { + String localName = customisableType.toPrefixString(namespaceService).replace(":", ""); + localName = MessageFormat.format("{0}CustomProperties", localName); + return QName.createQName(RM_CUSTOM_URI, localName); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminService#isCustomisable(org.alfresco.service.namespace.QName) + */ + @Override + public boolean isCustomisable(QName type) + { + ParameterCheck.mandatory("type", type); + return getCustomisable().contains(type); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminService#makeCustomisable(org.alfresco.service.namespace.QName) + */ + @Override + public void makeCustomisable(QName type) + { + ParameterCheck.mandatory("type", type); + + if (customisableTypes == null) + { + // Add the type to the pending list + pendingCustomisableTypes.add(type); + } + else + { + QName customAspect = getCustomAspect(type); + if (dictionaryService.getAspect(customAspect) == null) + { + NodeRef modelRef = getCustomModelRef(customAspect.getNamespaceURI()); + M2Model model = readCustomContentModel(modelRef); + try + { + // Create the new aspect to hold the custom properties + M2Aspect aspect = model.createAspect(customAspect.toPrefixString(namespaceService)); + aspect.setDescription(type.toPrefixString(namespaceService)); + } + finally + { + writeCustomContentModel(modelRef, model); + } + customisableTypes.put(type, customAspect); + } + } + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminService#unmakeCustomisable(org.alfresco.service.namespace.QName) + */ + @Override + public void unmakeCustomisable(QName type) + { + ParameterCheck.mandatory("type", type); + + if (customisableTypes == null) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_SERVICE_NOT_INIT)); + } + + QName customAspect = getCustomAspect(type); + if (dictionaryService.getAspect(customAspect) != null) + { + // TODO need to confirm that the custom properties are not being used! + + NodeRef modelRef = getCustomModelRef(customAspect.getNamespaceURI()); + M2Model model = readCustomContentModel(modelRef); + try + { + // Create the new aspect to hold the custom properties + model.removeAspect(customAspect.toPrefixString(namespaceService)); + } + finally + { + writeCustomContentModel(modelRef, model); + } + customisableTypes.remove(type); + } + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminService#existsCustomProperty(org.alfresco.service.namespace.QName) + */ + @Override + public boolean existsCustomProperty(QName propertyName) + { + ParameterCheck.mandatory("propertyName", propertyName); + + boolean result = false; + if (RM_CUSTOM_URI.equals(propertyName.getNamespaceURI()) == true && + dictionaryService.getProperty(propertyName) != null) + { + result = true; + } + return result; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminService#getCustomPropertyDefinitions() + */ + public Map getCustomPropertyDefinitions() + { + Map result = new HashMap(); + for (QName customisableType : getCustomisable()) + { + result.putAll(getCustomPropertyDefinitions(customisableType)); + } + return result; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminService#getCustomPropertyDefinitions(org.alfresco.module.org_alfresco_module_rm.CustomisableRmElement) + */ + public Map getCustomPropertyDefinitions(QName customisableType) + { + QName relevantAspectQName = getCustomAspect(customisableType); + AspectDefinition aspectDefn = dictionaryService.getAspect(relevantAspectQName); + Map propDefns = aspectDefn.getProperties(); + + return propDefns; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminService#addCustomPropertyDefinition(org.alfresco.service.namespace.QName, org.alfresco.service.namespace.QName, java.lang.String, org.alfresco.service.namespace.QName, java.lang.String, java.lang.String) + */ + public QName addCustomPropertyDefinition(QName propId, QName aspectName, String label, QName dataType, String title, String description) + { + return addCustomPropertyDefinition(propId, aspectName, label, dataType, title, description, null, false, false, false, null); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminService#addCustomPropertyDefinition(org.alfresco.service.namespace.QName, org.alfresco.service.namespace.QName, java.lang.String, org.alfresco.service.namespace.QName, java.lang.String, java.lang.String, java.lang.String, boolean, boolean, boolean, org.alfresco.service.namespace.QName) + */ + public QName addCustomPropertyDefinition(QName propId, QName aspectName, String label, QName dataType, String title, String description, String defaultValue, boolean multiValued, boolean mandatory, boolean isProtected, QName lovConstraint) + { + if (isCustomisable(aspectName) == false) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_NOT_CUSTOMISABLE, aspectName.toPrefixString(namespaceService))); + } + + // title parameter is currently ignored. Intentionally. + if (propId == null) + { + // Generate a propId + propId = this.generateQNameFor(label); + } + + ParameterCheck.mandatory("aspectName", aspectName); + ParameterCheck.mandatory("label", label); + ParameterCheck.mandatory("dataType", dataType); + + NodeRef modelRef = getCustomModelRef(propId.getNamespaceURI()); + M2Model deserializedModel = readCustomContentModel(modelRef); + + QName customAspect = getCustomAspect(aspectName); + M2Aspect customPropsAspect = deserializedModel.getAspect(customAspect.toPrefixString(namespaceService)); + + if (customPropsAspect == null) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_INVALID_CUSTOM_ASPECT, customAspect, aspectName.toPrefixString(namespaceService))); + } + + String propIdAsString = propId.toPrefixString(namespaceService); + M2Property customProp = customPropsAspect.getProperty(propIdAsString); + if (customProp != null) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_PROPERTY_ALREADY_EXISTS, propIdAsString)); + } + + M2Property newProp = customPropsAspect.createProperty(propIdAsString); + newProp.setName(propIdAsString); + newProp.setType(dataType.toPrefixString(namespaceService)); + + // Note that the title is used to store the RM 'label'. + newProp.setTitle(label); + newProp.setDescription(description); + newProp.setDefaultValue(defaultValue); + + newProp.setMandatory(mandatory); + newProp.setProtected(isProtected); + newProp.setMultiValued(multiValued); + + newProp.setIndexed(true); + newProp.setIndexedAtomically(true); + newProp.setStoredInIndex(false); + newProp.setIndexTokenisationMode(IndexTokenisationMode.FALSE); + + if (lovConstraint != null) + { + if (! dataType.equals(DataTypeDefinition.TEXT)) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_CANNOT_APPLY_CONSTRAINT, lovConstraint, propIdAsString, dataType)); + } + + String lovConstraintQNameAsString = lovConstraint.toPrefixString(namespaceService); + newProp.addConstraintRef(lovConstraintQNameAsString); + } + + writeCustomContentModel(modelRef, deserializedModel); + + if (logger.isInfoEnabled()) + { + logger.info("addCustomPropertyDefinition: "+label+ + "=" + propIdAsString + " to aspect: "+aspectName); + } + + return propId; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminService#setCustomPropertyDefinitionLabel(org.alfresco.service.namespace.QName, java.lang.String) + */ + public QName setCustomPropertyDefinitionLabel(QName propQName, String newLabel) + { + ParameterCheck.mandatory("propQName", propQName); + + PropertyDefinition propDefn = dictionaryService.getProperty(propQName); + if (propDefn == null) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_PROP_EXIST, propQName)); + } + + if (newLabel == null) return propQName; + + NodeRef modelRef = getCustomModelRef(propQName.getNamespaceURI()); + M2Model deserializedModel = readCustomContentModel(modelRef); + + M2Property targetProperty = findProperty(propQName, deserializedModel); + + targetProperty.setTitle(newLabel); + writeCustomContentModel(modelRef, deserializedModel); + + if (logger.isInfoEnabled()) + { + logger.info("setCustomPropertyDefinitionLabel: "+propQName+ + "=" + newLabel); + } + + return propQName; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminService#setCustomPropertyDefinitionConstraint(org.alfresco.service.namespace.QName, org.alfresco.service.namespace.QName) + */ + public QName setCustomPropertyDefinitionConstraint(QName propQName, QName newLovConstraint) + { + ParameterCheck.mandatory("propQName", propQName); + + PropertyDefinition propDefn = dictionaryService.getProperty(propQName); + if (propDefn == null) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_PROP_EXIST, propQName)); + } + + NodeRef modelRef = getCustomModelRef(propQName.getNamespaceURI()); + M2Model deserializedModel = readCustomContentModel(modelRef); + + M2Property targetProp = findProperty(propQName, deserializedModel); + String dataType = targetProp.getType(); + + if (! dataType.equals(DataTypeDefinition.TEXT.toPrefixString(namespaceService))) + { + + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_CANNOT_APPLY_CONSTRAINT, newLovConstraint, targetProp.getName(), dataType)); + } + String lovConstraintQNameAsString = newLovConstraint.toPrefixString(namespaceService); + + // Add the constraint - if it isn't already there. + String refOfExistingConstraint = null; + + for (M2Constraint c : targetProp.getConstraints()) + { + // There should only be one constraint. + refOfExistingConstraint = c.getRef(); + break; + } + if (refOfExistingConstraint != null) + { + targetProp.removeConstraintRef(refOfExistingConstraint); + } + targetProp.addConstraintRef(lovConstraintQNameAsString); + + writeCustomContentModel(modelRef, deserializedModel); + + if (logger.isInfoEnabled()) + { + logger.info("addCustomPropertyDefinitionConstraint: "+lovConstraintQNameAsString); + } + + return propQName; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminService#removeCustomPropertyDefinitionConstraints(org.alfresco.service.namespace.QName) + */ + public QName removeCustomPropertyDefinitionConstraints(QName propQName) + { + ParameterCheck.mandatory("propQName", propQName); + + PropertyDefinition propDefn = dictionaryService.getProperty(propQName); + if (propDefn == null) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_PROP_EXIST, propQName)); + } + + NodeRef modelRef = getCustomModelRef(propQName.getNamespaceURI()); + M2Model deserializedModel = readCustomContentModel(modelRef); + + M2Property targetProperty = findProperty(propQName, deserializedModel); + + // Need to count backwards to remove constraints + for (int i = targetProperty.getConstraints().size() - 1; i >= 0; i--) { + String ref = targetProperty.getConstraints().get(i).getRef(); + targetProperty.removeConstraintRef(ref); + } + + writeCustomContentModel(modelRef, deserializedModel); + + if (logger.isInfoEnabled()) + { + logger.info("removeCustomPropertyDefinitionConstraints: "+propQName); + } + + return propQName; + } + + /** + * + * @param propQName + * @param deserializedModel + * @return + */ + private M2Property findProperty(QName propQName, M2Model deserializedModel) + { + List aspects = deserializedModel.getAspects(); + // Search through the aspects looking for the custom property + for (M2Aspect aspect : aspects) + { + for (M2Property prop : aspect.getProperties()) + { + if (propQName.toPrefixString(namespaceService).equals(prop.getName())) + { + return prop; + } + } + } + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_CUSTOM_PROP_EXIST, propQName)); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminService#removeCustomPropertyDefinition(org.alfresco.service.namespace.QName) + */ + public void removeCustomPropertyDefinition(QName propQName) + { + ParameterCheck.mandatory("propQName", propQName); + + NodeRef modelRef = getCustomModelRef(propQName.getNamespaceURI()); + M2Model deserializedModel = readCustomContentModel(modelRef); + + String propQNameAsString = propQName.toPrefixString(namespaceService); + + String aspectName = null; + + boolean found = false; + + // Need to select the correct aspect in the customModel from which we'll + // attempt to delete the property definition. + for (QName customisableType : getCustomisable()) + { + aspectName = getCustomAspect(customisableType).toPrefixString(namespaceService); + M2Aspect customPropsAspect = deserializedModel.getAspect(aspectName); + + if (customPropsAspect == null) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_UNKNOWN_ASPECT, aspectName)); + } + + M2Property prop = customPropsAspect.getProperty(propQNameAsString); + if (prop != null) + { + if (logger.isDebugEnabled()) + { + StringBuilder msg = new StringBuilder(); + msg.append("Attempting to delete custom property: "); + msg.append(propQNameAsString); + logger.debug(msg.toString()); + } + + found = true; + customPropsAspect.removeProperty(propQNameAsString); + break; + } + } + + if (found == false) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_PROP_EXIST, propQNameAsString)); + } + + writeCustomContentModel(modelRef, deserializedModel); + + if (logger.isInfoEnabled()) + { + logger.info("deleteCustomPropertyDefinition: "+propQNameAsString+" from aspect: "+aspectName); + } + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminService#getCustomReferenceDefinitions() + */ + public Map getCustomReferenceDefinitions() + { + QName relevantAspectQName = QName.createQName(RMC_CUSTOM_ASSOCS, namespaceService); + AspectDefinition aspectDefn = dictionaryService.getAspect(relevantAspectQName); + Map assocDefns = aspectDefn.getAssociations(); + + return assocDefns; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminService#addCustomReference(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName) + */ + public void addCustomReference(NodeRef fromNode, NodeRef toNode, QName refId) + { + // Check that a definition for the reference type exists. + Map availableAssocs = this.getCustomReferenceDefinitions(); + + AssociationDefinition assocDef = availableAssocs.get(refId); + if (assocDef == null) + { + throw new IllegalArgumentException(I18NUtil.getMessage(MSG_REF_EXIST, refId)); + } + + // Check if an instance of this reference type already exists in the same direction. + boolean associationAlreadyExists = false; + if (assocDef.isChild()) + { + List childAssocs = nodeService.getChildAssocs(fromNode, assocDef.getName(), assocDef.getName()); + for (ChildAssociationRef chAssRef : childAssocs) + { + if (chAssRef.getChildRef().equals(toNode)) + { + associationAlreadyExists = true; + } + } + } + else + { + List assocs = nodeService.getTargetAssocs(fromNode, assocDef.getName()); + for (AssociationRef assRef : assocs) + { + if (assRef.getTargetRef().equals(toNode)) + { + associationAlreadyExists = true; + } + } + } + if (associationAlreadyExists) + { + StringBuilder msg = new StringBuilder(); + msg.append("Association '").append(refId).append("' already exists from ") + .append(fromNode).append(" to ").append(toNode); + throw new AlfrescoRuntimeException(msg.toString()); + } + + // Invoke before create reference policy + invokeBeforeCreateReference(fromNode, toNode, refId); + + if (assocDef.isChild()) + { + this.nodeService.addChild(fromNode, toNode, refId, refId); + } + else + { + this.nodeService.createAssociation(fromNode, toNode, refId); + } + + // Invoke on create reference policy + invokeOnCreateReference(fromNode, toNode, refId); + } + + public void removeCustomReference(NodeRef fromNode, NodeRef toNode, QName assocId) + { + Map availableAssocs = this.getCustomReferenceDefinitions(); + + AssociationDefinition assocDef = availableAssocs.get(assocId); + if (assocDef == null) + { + throw new IllegalArgumentException(I18NUtil.getMessage(MSG_REF_EXIST, assocId)); + } + + invokeBeforeRemoveReference(fromNode, toNode, assocId); + + if (assocDef.isChild()) + { + // TODO: Ask for a more efficient method such as + // nodeService.removeChildAssociation(fromNode, toNode, chRef.getTypeQName(), null); + + List children = nodeService.getChildAssocs(fromNode); + for (ChildAssociationRef chRef : children) + { + if (assocId.equals(chRef.getTypeQName()) && chRef.getChildRef().equals(toNode)) + { + nodeService.removeChildAssociation(chRef); + } + } + } + else + { + nodeService.removeAssociation(fromNode, toNode, assocId); + } + + invokeOnRemoveReference(fromNode, toNode, assocId); + } + + public List getCustomReferencesFrom(NodeRef node) + { + List retrievedAssocs = nodeService.getTargetAssocs(node, RegexQNamePattern.MATCH_ALL); + return retrievedAssocs; + } + + public List getCustomChildReferences(NodeRef node) + { + List childAssocs = nodeService.getChildAssocs(node); + return childAssocs; + } + + public List getCustomReferencesTo(NodeRef node) + { + List retrievedAssocs = nodeService.getSourceAssocs(node, RegexQNamePattern.MATCH_ALL); + return retrievedAssocs; + } + + public List getCustomParentReferences(NodeRef node) + { + List result = nodeService.getParentAssocs(node); + return result; + } + + // note: currently RMC custom assocs only + public QName addCustomAssocDefinition(String label) + { + ParameterCheck.mandatoryString("label", label); + + NodeRef modelRef = getCustomModelRef(""); // defaults to RM_CUSTOM_URI + M2Model deserializedModel = readCustomContentModel(modelRef); + + String aspectName = RecordsManagementAdminServiceImpl.RMC_CUSTOM_ASSOCS; + + M2Aspect customAssocsAspect = deserializedModel.getAspect(aspectName); + + if (customAssocsAspect == null) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_UNKNOWN_ASPECT, aspectName)); + } + + // If this label is already taken... + if (getQNameForClientId(label) != null) + { + throw new IllegalArgumentException(I18NUtil.getMessage(MSG_REF_LABEL_IN_USE, label)); + } + + QName generatedQName = this.generateQNameFor(label); + String generatedShortQName = generatedQName.toPrefixString(namespaceService); + + M2ClassAssociation customAssoc = customAssocsAspect.getAssociation(generatedShortQName); + if (customAssoc != null) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_ASSOC_EXISTS, generatedShortQName)); + } + + M2Association newAssoc = customAssocsAspect.createAssociation(generatedShortQName); + newAssoc.setSourceMandatory(false); + newAssoc.setTargetMandatory(false); + + // MOB-1573 + newAssoc.setSourceMany(true); + newAssoc.setTargetMany(true); + + // The label is stored in the title. + newAssoc.setTitle(label); + + // TODO Could be the customAssocs aspect + newAssoc.setTargetClassName(RecordsManagementAdminServiceImpl.RMA_RECORD); + + writeCustomContentModel(modelRef, deserializedModel); + + if (logger.isInfoEnabled()) + { + logger.info("addCustomAssocDefinition: ("+label+")"); + } + + return generatedQName; + } + + // note: currently RMC custom assocs only + public QName addCustomChildAssocDefinition(String source, String target) + { + ParameterCheck.mandatoryString("source", source); + ParameterCheck.mandatoryString("target", target); + + NodeRef modelRef = getCustomModelRef(""); // defaults to RM_CUSTOM_URI + M2Model deserializedModel = readCustomContentModel(modelRef); + + String aspectName = RecordsManagementAdminServiceImpl.RMC_CUSTOM_ASSOCS; + + M2Aspect customAssocsAspect = deserializedModel.getAspect(aspectName); + + if (customAssocsAspect == null) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_UNKNOWN_ASPECT, aspectName)); + } + + String compoundID = this.getCompoundIdFor(source, target); + if (getQNameForClientId(compoundID) != null) + { + throw new IllegalArgumentException(I18NUtil.getMessage(MSG_REF_LABEL_IN_USE, compoundID)); + } + + M2ClassAssociation customAssoc = customAssocsAspect.getAssociation(compoundID); + if (customAssoc != null) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_CHILD_ASSOC_EXISTS, compoundID)); + } + QName generatedQName = this.generateQNameFor(compoundID); + + M2ChildAssociation newAssoc = customAssocsAspect.createChildAssociation(generatedQName.toPrefixString(namespaceService)); + newAssoc.setSourceMandatory(false); + newAssoc.setTargetMandatory(false); + + // MOB-1573 + newAssoc.setSourceMany(true); + newAssoc.setTargetMany(true); + + // source and target are stored in title. + newAssoc.setTitle(compoundID); + + // TODO Could be the custom assocs aspect + newAssoc.setTargetClassName(RecordsManagementAdminServiceImpl.RMA_RECORD); + + writeCustomContentModel(modelRef, deserializedModel); + + if (logger.isInfoEnabled()) + { + logger.info("addCustomChildAssocDefinition: ("+source+","+target+")"); + } + + return generatedQName; + } + + // note: currently RMC custom assocs only + public QName updateCustomChildAssocDefinition(QName refQName, String newSource, String newTarget) + { + String compoundId = getCompoundIdFor(newSource, newTarget); + return persistUpdatedAssocTitle(refQName, compoundId); + } + + // note: currently RMC custom assocs only + public QName updateCustomAssocDefinition(QName refQName, String newLabel) + { + return persistUpdatedAssocTitle(refQName, newLabel); + } + + /** + * This method writes the specified String into the association's title property. + * For RM custom properties and references, Title is used to store the identifier. + */ + // note: currently RMC custom assocs only + private QName persistUpdatedAssocTitle(QName refQName, String newTitle) + { + ParameterCheck.mandatory("refQName", refQName); + + AssociationDefinition assocDefn = dictionaryService.getAssociation(refQName); + if (assocDefn == null) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_CONNOT_FIND_ASSOC_DEF, refQName)); + } + + NodeRef modelRef = getCustomModelRef(""); // defaults to RM_CUSTOM_URI + M2Model deserializedModel = readCustomContentModel(modelRef); + + M2Aspect customAssocsAspect = deserializedModel.getAspect(RMC_CUSTOM_ASSOCS); + + for (M2ClassAssociation assoc : customAssocsAspect.getAssociations()) + { + if (refQName.toPrefixString(namespaceService).equals(assoc.getName())) + { + if (newTitle != null) + { + assoc.setTitle(newTitle); + } + } + } + writeCustomContentModel(modelRef, deserializedModel); + + if (logger.isInfoEnabled()) + { + logger.info("persistUpdatedAssocTitle: "+refQName+ + "=" + newTitle + " to aspect: " + RMC_CUSTOM_ASSOCS); + } + + return refQName; + } + + public void addCustomConstraintDefinition(QName constraintName, String title, boolean caseSensitive, List allowedValues, MatchLogic matchLogic) + { + ParameterCheck.mandatory("constraintName", constraintName); + ParameterCheck.mandatoryString("title", title); + ParameterCheck.mandatory("allowedValues", allowedValues); + + NodeRef modelRef = getCustomModelRef(constraintName.getNamespaceURI()); + M2Model deserializedModel = readCustomContentModel(modelRef); + + String constraintNameAsPrefixString = constraintName.toPrefixString(namespaceService); + + M2Constraint customConstraint = deserializedModel.getConstraint(constraintNameAsPrefixString); + if (customConstraint != null) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_CONSTRAINT_EXISTS, constraintNameAsPrefixString)); + } + + M2Constraint newCon = deserializedModel.createConstraint(constraintNameAsPrefixString, CUSTOM_CONSTRAINT_TYPE); + + newCon.setTitle(title); + newCon.createParameter(PARAM_ALLOWED_VALUES, allowedValues); + newCon.createParameter(PARAM_CASE_SENSITIVE, caseSensitive ? "true" : "false"); + newCon.createParameter(PARAM_MATCH_LOGIC, matchLogic.toString()); + + writeCustomContentModel(modelRef, deserializedModel); + + if (logger.isInfoEnabled()) + { + logger.info("addCustomConstraintDefinition: "+constraintNameAsPrefixString+" (valueCnt: "+allowedValues.size()+")"); + } + } + + /* + public void addCustomConstraintDefinition(QName constraintName, String description, Map parameters) + { + // TODO Auto-generated method stub + } + */ + + public void changeCustomConstraintValues(QName constraintName, List newAllowedValues) + { + ParameterCheck.mandatory("constraintName", constraintName); + ParameterCheck.mandatory("newAllowedValues", newAllowedValues); + + NodeRef modelRef = getCustomModelRef(constraintName.getNamespaceURI()); + M2Model deserializedModel = readCustomContentModel(modelRef); + + String constraintNameAsPrefixString = constraintName.toPrefixString(namespaceService); + + M2Constraint customConstraint = deserializedModel.getConstraint(constraintNameAsPrefixString); + if (customConstraint == null) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_CANNOT_FIND_CONSTRAINT, constraintNameAsPrefixString)); + } + + String type = customConstraint.getType(); + if ((type == null) || (! type.equals(CUSTOM_CONSTRAINT_TYPE))) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_UNEXPECTED_TYPE_CONSTRAINT, type, constraintNameAsPrefixString, CUSTOM_CONSTRAINT_TYPE)); + } + + customConstraint.removeParameter(PARAM_ALLOWED_VALUES); + customConstraint.createParameter(PARAM_ALLOWED_VALUES, newAllowedValues); + + writeCustomContentModel(modelRef, deserializedModel); + + if (logger.isInfoEnabled()) + { + logger.info("changeCustomConstraintValues: "+constraintNameAsPrefixString+" (valueCnt: "+newAllowedValues.size()+")"); + } + } + + public void changeCustomConstraintTitle(QName constraintName, String title) + { + ParameterCheck.mandatory("constraintName", constraintName); + ParameterCheck.mandatoryString("title", title); + + NodeRef modelRef = getCustomModelRef(constraintName.getNamespaceURI()); + M2Model deserializedModel = readCustomContentModel(modelRef); + + String constraintNameAsPrefixString = constraintName.toPrefixString(namespaceService); + + M2Constraint customConstraint = deserializedModel.getConstraint(constraintNameAsPrefixString); + if (customConstraint == null) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_CANNOT_FIND_CONSTRAINT, constraintNameAsPrefixString)); + } + + String type = customConstraint.getType(); + if ((type == null) || (! type.equals(CUSTOM_CONSTRAINT_TYPE))) + { + + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_UNEXPECTED_TYPE_CONSTRAINT, type, constraintNameAsPrefixString, CUSTOM_CONSTRAINT_TYPE)); + } + + customConstraint.setTitle(title); + + writeCustomContentModel(modelRef, deserializedModel); + + if (logger.isInfoEnabled()) + { + logger.info("changeCustomConstraintTitle: "+constraintNameAsPrefixString+" (title: "+title+")"); + } + } + + public List getCustomConstraintDefinitions(QName modelQName) + { + Collection conDefs = dictionaryService.getConstraints(modelQName, true); + + for (ConstraintDefinition conDef : conDefs) + { + Constraint con = conDef.getConstraint(); + if (! (con instanceof RMListOfValuesConstraint)) + { + conDefs.remove(conDef); + } + } + + return new ArrayList(conDefs); + } + + public void removeCustomConstraintDefinition(QName constraintName) + { + ParameterCheck.mandatory("constraintName", constraintName); + + NodeRef modelRef = getCustomModelRef(constraintName.getNamespaceURI()); + M2Model deserializedModel = readCustomContentModel(modelRef); + + String constraintNameAsPrefixString = constraintName.toPrefixString(namespaceService); + + M2Constraint customConstraint = deserializedModel.getConstraint(constraintNameAsPrefixString); + if (customConstraint == null) + { + + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_CANNOT_FIND_CONSTRAINT, constraintNameAsPrefixString)); + } + + deserializedModel.removeConstraint(constraintNameAsPrefixString); + + writeCustomContentModel(modelRef, deserializedModel); + + if (logger.isInfoEnabled()) + { + logger.info("deleteCustomConstraintDefinition: "+constraintNameAsPrefixString); + } + } + + private NodeRef getCustomModelRef(String uri) + { + if ((uri.equals("")) || (uri.equals(RecordsManagementModel.RM_CUSTOM_URI))) + { + // note: short-cut for "rmc" currently assumes that RM custom model does not define additional namespaces + return RM_CUSTOM_MODEL_NODE_REF; + } + else + { + // ALF-5875 + List modelRefs = dictonaryRepositoryBootstrap.getModelRefs(); + + for (NodeRef modelRef : modelRefs) + { + try + { + M2Model model = readCustomContentModel(modelRef); + + for (M2Namespace namespace : model.getNamespaces()) + { + if (namespace.getUri().equals(uri)) + { + return modelRef; + } + } + } + catch (DictionaryException de) + { + logger.warn("readCustomContentModel: skip model ("+modelRef+") whilst searching for uri ("+uri+"): "+de); + } + } + + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_CUSTOM_MODEL_NOT_FOUND, uri)); + } + } + + private M2Model readCustomContentModel(NodeRef modelNodeRef) + { + ContentReader reader = this.contentService.getReader(modelNodeRef, + ContentModel.TYPE_CONTENT); + + if (reader.exists() == false) {throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_CUSTOM_MODEL_NO_CONTENT, modelNodeRef.toString()));} + + InputStream contentIn = null; + M2Model deserializedModel = null; + try + { + contentIn = reader.getContentInputStream(); + deserializedModel = M2Model.createModel(contentIn); + } + finally + { + try + { + if (contentIn != null) contentIn.close(); + } + catch (IOException ignored) + { + // Intentionally empty.` + } + } + return deserializedModel; + } + + private void writeCustomContentModel(NodeRef modelRef, M2Model deserializedModel) + { + ContentWriter writer = this.contentService.getWriter(modelRef, ContentModel.TYPE_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_XML); + writer.setEncoding("UTF-8"); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + deserializedModel.toXML(baos); + + String updatedModelXml; + try + { + updatedModelXml = baos.toString("UTF-8"); + writer.putContent(updatedModelXml); + // putContent closes all resources. + // so we don't have to. + } catch (UnsupportedEncodingException uex) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_ERROR_WRITE_CUSTOM_MODEL, modelRef.toString()), uex); + } + } + + + public QName getQNameForClientId(String localName) + { + //TODO 1. After certification. This implementation currently does not support reference, + // property, constraints definitions with the same names, which is technically allowed by Alfresco. + + //TODO 2. Note the implicit assumption here that all custom references will have + // unique titles. This is, in fact, not guaranteed. + + QName propertyResult = null; + for (QName qn : getCustomPropertyDefinitions().keySet()) + { + if (localName != null && localName.equals(qn.getLocalName())) + { + propertyResult = qn; + } + } + + if (propertyResult != null) + { + return propertyResult; + } + + QName referenceResult = null; + for (QName refQn : getCustomReferenceDefinitions().keySet()) + { + if (localName != null && localName.equals(refQn.getLocalName())) + { + referenceResult = refQn; + } + } + + // TODO Handle the case where both are not null + return referenceResult; + } + + private QName generateQNameFor(String clientId) + { + if (getQNameForClientId(clientId) != null) + { + // TODO log it's already taken. What to do? + throw new IllegalArgumentException(I18NUtil.getMessage(MSG_ERROR_CLIENT_ID, clientId)); + } + + String newGUID = GUID.generate(); + QName newQName = QName.createQName(RM_CUSTOM_PREFIX, newGUID, namespaceService); + + return newQName; + } + + public String[] splitSourceTargetId(String sourceTargetId) + { + if (!sourceTargetId.contains(SOURCE_TARGET_ID_SEPARATOR)) + { + throw new IllegalArgumentException(I18NUtil.getMessage(MSG_ERROR_SPLIT_ID, sourceTargetId, SOURCE_TARGET_ID_SEPARATOR)); + } + return sourceTargetId.split(SOURCE_TARGET_ID_SEPARATOR); + } + + public String getCompoundIdFor(String sourceId, String targetId) + { + ParameterCheck.mandatoryString("sourceId", sourceId); + ParameterCheck.mandatoryString("targetId", targetId); + + if (sourceId.contains(SOURCE_TARGET_ID_SEPARATOR)) + { + throw new IllegalArgumentException("sourceId cannot contain '" + SOURCE_TARGET_ID_SEPARATOR + + "': " + sourceId); + } + StringBuilder result = new StringBuilder(); + result.append(sourceId) + .append(SOURCE_TARGET_ID_SEPARATOR) + .append(targetId); + return result.toString(); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/RecordsManagementBootstrap.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/RecordsManagementBootstrap.java new file mode 100644 index 0000000000..1989960c7d --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/RecordsManagementBootstrap.java @@ -0,0 +1,105 @@ +/* + * 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.module.org_alfresco_module_rm; + +import org.alfresco.module.org_alfresco_module_rm.caveat.RMCaveatConfigService; +import org.alfresco.module.org_alfresco_module_rm.email.CustomEmailMappingService; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.transaction.TransactionService; +import org.springframework.context.ApplicationEvent; +import org.springframework.extensions.surf.util.AbstractLifecycleBean; + + +/** + * RM module bootstrap + * + * @author janv + */ +public class RecordsManagementBootstrap extends AbstractLifecycleBean +{ + private TransactionService transactionService; + private RMCaveatConfigService caveatConfigService; + private CustomEmailMappingService customEmailMappingService; + private RecordsManagementAdminService adminService; + + public void setTransactionService(TransactionService transactionService) + { + this.transactionService = transactionService; + } + + public void setCaveatConfigService(RMCaveatConfigService caveatConfigService) + { + this.caveatConfigService = caveatConfigService; + } + + public void setCustomEmailMappingService(CustomEmailMappingService customEmailMappingService) + { + this.customEmailMappingService = customEmailMappingService; + } + + public void setRecordsManagementAdminService(RecordsManagementAdminService adminService) + { + this.adminService = adminService; + } + + public CustomEmailMappingService getCustomEmailMappingService() + { + return customEmailMappingService; + } + + @Override + protected void onBootstrap(ApplicationEvent event) + { + // run as System on bootstrap + AuthenticationUtil.runAs(new RunAsWork() + { + public Object doWork() + { + RetryingTransactionCallback callback = new RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + // initialise caveat config + caveatConfigService.init(); + + // initialise custom email mapping + customEmailMappingService.init(); + + // Initialise the custom model + adminService.initialiseCustomModel(); + + return null; + } + }; + transactionService.getRetryingTransactionHelper().doInTransaction(callback); + + return null; + } + }, AuthenticationUtil.getSystemUserName()); + } + + @Override + protected void onShutdown(ApplicationEvent event) + { + // NOOP + } +} + diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/RecordsManagementPolicies.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/RecordsManagementPolicies.java new file mode 100644 index 0000000000..972170d384 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/RecordsManagementPolicies.java @@ -0,0 +1,79 @@ +/* + * 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.module.org_alfresco_module_rm; + +import java.io.Serializable; +import java.util.Map; + +import org.alfresco.repo.policy.ClassPolicy; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; + +/** + * Interface containing records management policies + * + * @author Roy Wetherall + */ +public interface RecordsManagementPolicies +{ + /** Policy names */ + public static final QName BEFORE_RM_ACTION_EXECUTION = QName.createQName(NamespaceService.ALFRESCO_URI, "beforeRMActionExecution"); + public static final QName ON_RM_ACTION_EXECUTION = QName.createQName(NamespaceService.ALFRESCO_URI, "onRMActionExecution"); + public static final QName BEFORE_CREATE_REFERENCE = QName.createQName(NamespaceService.ALFRESCO_URI, "beforeCreateReference"); + public static final QName ON_CREATE_REFERENCE = QName.createQName(NamespaceService.ALFRESCO_URI, "onCreateReference"); + public static final QName BEFORE_REMOVE_REFERENCE = QName.createQName(NamespaceService.ALFRESCO_URI, "beforeRemoveReference"); + public static final QName ON_REMOVE_REFERENCE = QName.createQName(NamespaceService.ALFRESCO_URI, "onRemoveReference"); + + /** Before records management action execution */ + public interface BeforeRMActionExecution extends ClassPolicy + { + public void beforeRMActionExecution(NodeRef nodeRef, String name, Map parameters); + } + + /** On records management action execution */ + public interface OnRMActionExecution extends ClassPolicy + { + public void onRMActionExecution(NodeRef nodeRef, String name, Map parameters); + } + + /** Before creation of reference */ + public interface BeforeCreateReference extends ClassPolicy + { + public void beforeCreateReference(NodeRef fromNodeRef, NodeRef toNodeRef, QName reference); + } + + /** On creation of reference */ + public interface OnCreateReference extends ClassPolicy + { + public void onCreateReference(NodeRef fromNodeRef, NodeRef toNodeRef, QName reference); + } + + /** Before removal of reference */ + public interface BeforeRemoveReference extends ClassPolicy + { + public void beforeRemoveReference(NodeRef fromNodeRef, NodeRef toNodeRef, QName reference); + } + + /** On removal of reference */ + public interface OnRemoveReference extends ClassPolicy + { + public void onRemoveReference(NodeRef fromNodeRef, NodeRef toNodeRef, QName reference); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/RecordsManagementPoliciesUtil.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/RecordsManagementPoliciesUtil.java new file mode 100644 index 0000000000..b281008a86 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/RecordsManagementPoliciesUtil.java @@ -0,0 +1,71 @@ +/* + * 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.module.org_alfresco_module_rm; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.service.cmr.repository.InvalidNodeRefException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.QName; + +/** + * @author Roy Wetherall + */ +public class RecordsManagementPoliciesUtil +{ + /** + * Get all aspect and node type qualified names + * + * @param nodeRef + * the node we are interested in + * @return Returns a set of qualified names containing the node type and all + * the node aspects, or null if the node no longer exists + */ + public static Set getTypeAndAspectQNames(final NodeService nodeService, final NodeRef nodeRef) + { + return AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork>() + { + public Set doWork() throws Exception + { + Set qnames = null; + try + { + Set aspectQNames = nodeService.getAspects(nodeRef); + + QName typeQName = nodeService.getType(nodeRef); + + qnames = new HashSet(aspectQNames.size() + 1); + qnames.addAll(aspectQNames); + qnames.add(typeQName); + } + catch (InvalidNodeRefException e) + { + qnames = Collections.emptySet(); + } + // done + return qnames; + } + }, AuthenticationUtil.getAdminUserName()); + } + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/RecordsManagementService.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/RecordsManagementService.java new file mode 100644 index 0000000000..76a0f62af9 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/RecordsManagementService.java @@ -0,0 +1,462 @@ +/* + * 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.module.org_alfresco_module_rm; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.module.org_alfresco_module_rm.vital.VitalRecordDefinition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + +/** + * Records management service interface. + * + * Allows simple creation, manipulation and querying of records management components. + * + * @author Roy Wetherall + */ +public interface RecordsManagementService +{ + /********** RM Component methods **********/ + + /** + * Indicates whether the given node is a file plan component or not. + * + * @param nodeRef node reference + * @return boolean true if a file plan component, false otherwise + */ + boolean isFilePlanComponent(NodeRef nodeRef); + + /** + * Returns the 'kind' of file plan component the node reference is. + * Returns null if the given node reference is not a + * file plan component. + * + * @param nodeRef node reference + * @return FilePlanComponentKind the kind of file plan component the + * node is + * kind + * + * @since 2.0 + */ + FilePlanComponentKind getFilePlanComponentKind(NodeRef nodeRef); + + /** + * Indicates whether the given node is file plan node or not. + * + * @param nodeRef node reference + * @return boolean true if node is a file plan node + */ + boolean isFilePlan(NodeRef nodeRef); + + /** + * Indicates whether the given node is a record category or not. + * + * @param nodeRef node reference + * @return boolean true if records category, false otherwise + */ + boolean isRecordCategory(NodeRef nodeRef); + + /** + * Indicates whether the given node is a record folder or not. + * + * @param nodeRef node reference + * @return boolean true if record folder, false otherwise + */ + boolean isRecordFolder(NodeRef nodeRef); + + /** + * Indicates whether the given node is a record or not. + * + * @param nodeRef node reference + * @return boolean true if record, false otherwise + */ + boolean isRecord(NodeRef nodeRef); + + /** + * Indicates whether the given node is a hold (container) or not. + * + * @param nodeRef node reference + * @return boolean true if hold, false otherwise + * + * @since 2.0 + */ + boolean isHold(NodeRef nodeRef); + + /** + * Indicates whether the given node is a transfer (container) or not. + * + * @param nodeRef node reference + * @return boolean true if transfer, false otherwise + * + * @since 2.0 + */ + boolean isTransfer(NodeRef nodeRef); + + /** + * Indicates whether the given node (record or record folder) is a metadata stub or not. + * + * @param nodeRef node reference + * @return boolean true if a metadata stub, false otherwise + * + * @since + */ + boolean isMetadataStub(NodeRef nodeRef); + + /** + * Indicates whether the item is frozen or not. + * + * @param nodeRef node reference + * @return boolean true if record is frozen, false otherwise + * + * @since 2.0 + */ + boolean isFrozen(NodeRef nodeRef); + + + /** + * Indicates whether the item has frozen children or not. + * + * NOTE: this only checks the immediate children and does not check the frozen + * state of the node being passed + * + * @param nodeRef node reference + * @return boolean true if record folder has frozen children, false otherwise + * + * @since 2.0 + */ + boolean hasFrozenChildren(NodeRef nodeRef); + + /** + * Indicates whether the item is cutoff or not. + * + * @param nodeRef node reference + * @return boolean true if the item is cutoff, false otherwise + * + * @since 2.0 + */ + boolean isCutoff(NodeRef nodeRef); + + /** + * Gets the NodeRef sequence from the {@link #getFilePlan(NodeRef) root} + * down to the fileplan component given. The array will start with the NodeRef of the root + * and end with the name of the fileplan component node given. + * + * @param nodeRef a fileplan component + * @return Returns a NodeRef path starting with the name of the + * records management root + */ + List getNodeRefPath(NodeRef nodeRef); + + /** + * Gets the file plan the node is in. + * + * @return {@link NodeRef} file node reference, null if none + */ + NodeRef getFilePlan(NodeRef nodeRef); + + /********** File Plan Methods **********/ + + /** + * Gets all the file plan nodes. + * Searches the SpacesStore by default. + * + * @return List list of file plan nodes + */ + List getFilePlans(); + +// /** +// * Specify the store which should be searched. +// * +// * @see RecordsManagementService#getFilePlans() +// * +// * @param storeRef store reference +// * @return List list of record management root nodes +// */ +// @Deprecated +// List getRecordsManagementRoots(StoreRef storeRef); + + // TODO NodeRef getFilePlanById(String id); + + /** + * Creates a file plan as a child of the given parent node, with the name + * provided. + * + * @param parent parent node reference + * @param name name of the root + * @param type type of root created (must be sub-type of rm:filePlan) + * @return NodeRef node reference to the newly create RM root + */ + NodeRef createFilePlan(NodeRef parent, String name, QName type); + + /** + * @see #createFilePlan(NodeRef, String, QName) + * + * @param parent + * @param name + * @param type + * @param properties + * @return + */ + NodeRef createFilePlan(NodeRef parent, String name, QName type, Map properties); + + /** + * Creates a file plan with the default type. + * + * @see RecordsManagementService#createFilePlan(NodeRef, String, QName) + */ + NodeRef createFilePlan(NodeRef parent, String name); + + /** + * + * @param parent + * @param name + * @param properties + * @return + */ + NodeRef createFilePlan(NodeRef parent, String name, Map properties); + + // TODO void deleteRecordsManagementRoot(NodeRef root); + + /********** Record Category Methods **********/ + + // TODO NodeRef getRecordCategoryByPath(String path); + + // TODO NodeRef getRecordCategoryById(String id); + + // TODO NodeRef getRecordCategoryByName(NodeRef parent, String id); ?? + + /** + * Get all the items contained within a container. This will include record folders and other record categories. + * + * @param recordCategory record category node reference + * @param deep if true then return all children including sub-categories and their children in turn, if false then just + * return the immediate children + * @return {@link List}<{@link NodeRef>} list of contained node references + */ + List getAllContained(NodeRef recordCategory, boolean deep); + + /** + * Only return the immediate children. + * + * @see RecordsManagementService#getAllContained(NodeRef, boolean) + * + * @param recordCategory record category node reference + * @return {@link List}<{@link NodeRef>} list of contained node references + */ + List getAllContained(NodeRef recordCategory); + + /** + * Get all the record categories within a record category. + * + * @param recordCategory record category node reference + * @param deep if true then return all children including sub-categories and their children in turn, if false then just + * return the immediate children + * @return {@link List}<{@link NodeRef>} list of container node references + */ + List getContainedRecordCategories(NodeRef recordCategory, boolean deep); + + /** + * Only return immediate children. + * + * @see RecordsManagementService#getContainedRecordCategories(NodeRef, boolean) + * + * @param recordCategory container node reference + * @return {@link List}<{@link NodeRef>} list of container node references + */ + List getContainedRecordCategories(NodeRef recordCategory); + + /** + * Get all the record folders contained within a container + * + * @param container container node reference + * @param deep if true then return all children including sub-containers and their children in turn, if false then just + * return the immediate children + * @return {@link List}<{@link NodeRef>} list of record folder node references + */ + List getContainedRecordFolders(NodeRef container, boolean deep); + + /** + * Only return immediate children. + * + * @see RecordsManagementService#getContainedRecordFolders(NodeRef, boolean) + * + * @param container container node reference + * @return {@link List}<{@link NodeRef>} list of record folder node references + */ + List getContainedRecordFolders(NodeRef container); + + // TODO List getParentRecordCategories(NodeRef container); // also applicable to record folders + + /** + * Create a record category. + * + * @param parent parent node reference, must be a record category or file plan. + * @param name name of the new record category + * @param type type of container to create, must be a sub-type of rm:recordCategory + * @return NodeRef node reference of the created record category + */ + NodeRef createRecordCategory(NodeRef parent, String name, QName type); + + /** + * + * @param parent + * @param name + * @param type + * @param properties + * @return + */ + NodeRef createRecordCategory(NodeRef parent, String name, QName type, Map properties); + + /** + * Creates a record category of type rma:recordCategory + * + * @see RecordsManagementService#createRecordCategory(NodeRef, String, QName) + * + * @param parent parent node reference, must be a record category or file plan. + * @param name name of the record category + * @return NodeRef node reference of the created record category + */ + NodeRef createRecordCategory(NodeRef parent, String name); + + /** + * + * @param parent + * @param name + * @param properties + * @return + */ + NodeRef createRecordCategory(NodeRef parent, String name, Map properties); + + // TODO void deleteRecordCategory(NodeRef container); + + // TODO move, copy, link ?? + + /********** Record Folder methods **********/ + + /** + * Indicates whether the contents of a record folder are all declared. + * + * @param nodeRef node reference (record folder) + * @return boolean true if record folder contents are declared, false otherwise + */ + boolean isRecordFolderDeclared(NodeRef nodeRef); + + /** + * Indicates whether a record folder is closed or not. + * + * @param nodeRef node reference (record folder) + * @return boolean true if record folder is closed, false otherwise + * + * @since 2.0 + */ + boolean isRecordFolderClosed(NodeRef nodeRef); + + // TODO NodeRef getRecordFolderByPath(String path); + + // TODO NodeRef getRecordFolderById(String id); + + // TODO NodeRef getRecordFolderByName(NodeRef parent, String name); + + + /** + * Create a record folder in the rm container. The record folder with take the name and type + * provided. + * + * @param rmContainer records management container + * @param name name + * @param type type + * @return NodeRef node reference of record folder + */ + NodeRef createRecordFolder(NodeRef rmContainer, String name, QName type); + + /** + * + * @param rmContainer + * @param name + * @param type + * @param properties + * @return + */ + NodeRef createRecordFolder(NodeRef rmContainer, String name, QName type, Map properties); + + /** + * Type defaults to rm:recordFolder + * + * @see RecordsManagementService#createRecordCategory(NodeRef, String, QName) + * + * @param rmContainer records management container + * @param name name + * @return NodeRef node reference of record folder + */ + NodeRef createRecordFolder(NodeRef parent, String name); + + /** + * + * @param parent + * @param name + * @param properties + * @return + */ + NodeRef createRecordFolder(NodeRef parent, String name, Map properties); + + // TODO void deleteRecordFolder(NodeRef recordFolder); + + // TODO List getParentRecordsManagementContainers(NodeRef container); // also applicable to record folders + + /** + * Gets a list of all the records within a record folder + * + * @param recordFolder record folder + * @return List list of records in the record folder + */ + // TODO rename to getContainedRecords(NodeRef recordFolder); + List getRecords(NodeRef recordFolder); + + // TODO move? copy? link? + + /********** Record methods **********/ + + /** + * Get a list of all the record meta-data aspects + * + * @return {@link Set}<{@link QName}> list of record meta-data aspects + */ + Set getRecordMetaDataAspects(); + + /** + * Get all the record folders that a record is filed into. + * + * @param record the record node reference + * @return List list of folder record node references + */ + // TODO rename to List getParentRecordFolders(NodeRef record); + List getRecordFolders(NodeRef record); + + /** + * Indicates whether the record is declared + * + * @param nodeRef node reference (record) + * @return boolean true if record is declared, false otherwise + */ + boolean isRecordDeclared(NodeRef nodeRef); +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/RecordsManagementServiceImpl.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/RecordsManagementServiceImpl.java new file mode 100644 index 0000000000..520cebc015 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/RecordsManagementServiceImpl.java @@ -0,0 +1,1179 @@ +/* + * 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.module.org_alfresco_module_rm; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementCustomModel; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.repo.domain.node.NodeDAO; +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.policy.Behaviour.NotificationFrequency; +import org.alfresco.service.cmr.dictionary.AspectDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +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.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.alfresco.util.Pair; +import org.alfresco.util.ParameterCheck; +import org.springframework.extensions.surf.util.I18NUtil; + +/** + * Records management service implementation. + * + * @author Roy Wetherall + */ +public class RecordsManagementServiceImpl implements RecordsManagementService, + RecordsManagementModel, + RecordsManagementPolicies.OnCreateReference, + RecordsManagementPolicies.OnRemoveReference +{ + /** I18N */ + private final static String MSG_ERROR_ADD_CONTENT_CONTAINER = "rm.service.error-add-content-container"; + private final static String MSG_UPDATE_DISP_ACT_DEF = "rm.service.update-disposition-action-def"; + private final static String MSG_SET_ID = "rm.service.set-id"; + private final static String MSG_PATH_NODE = "rm.service.path-node"; + private final static String MSG_INVALID_RM_NODE = "rm.service.invalid-rm-node"; + private final static String MSG_NO_ROOT = "rm.service.no-root"; + private final static String MSG_DUP_ROOT = "rm.service.dup-root"; + private final static String MSG_ROOT_TYPE = "rm.service.root-type"; + private final static String MSG_CONTAINER_PARENT_TYPE= "rm.service.container-parent-type"; + private final static String MSG_CONTAINER_TYPE = "rm.service.container-type"; + private final static String MSG_CONTAINER_EXPECTED = "rm.service.container-expected"; + private final static String MSG_RECORD_FOLDER_EXPECTED = "rm.service.record-folder-expected"; + private final static String MSG_PARENT_RECORD_FOLDER_ROOT = "rm.service.parent-record-folder-root"; + private final static String MSG_PARENT_RECORD_FOLDER_TYPE = "rm.service.parent-record-folder-type"; + private final static String MSG_RECORD_FOLDER_TYPE = "rm.service.record-folder-type"; + private final static String MSG_NOT_RECORD = "rm.service.not-record"; + + /** Store that the RM roots are contained within */ + @SuppressWarnings("unused") + @Deprecated + private StoreRef defaultStoreRef = StoreRef.STORE_REF_WORKSPACE_SPACESSTORE; + + /** Service registry */ + private RecordsManagementServiceRegistry serviceRegistry; + + /** Dictionary service */ + private DictionaryService dictionaryService; + + /** Node service */ + private NodeService nodeService; + + /** Node DAO */ + private NodeDAO nodeDAO; + + /** Policy component */ + private PolicyComponent policyComponent; + + /** Records management action service */ + private RecordsManagementActionService rmActionService; + + /** Well-known location of the scripts folder. */ + private NodeRef scriptsFolderNodeRef = new NodeRef("workspace", "SpacesStore", "rm_scripts"); + + /** List of available record meta-data aspects */ + private Set recordMetaDataAspects; + + /** Java behaviour */ + private JavaBehaviour onChangeToDispositionActionDefinition; + + /** + * Set the service registry service + * + * @param serviceRegistry service registry + */ + public void setRecordsManagementServiceRegistry(RecordsManagementServiceRegistry serviceRegistry) + { + // Internal ops use the unprotected services from the voter (e.g. nodeService) + this.serviceRegistry = serviceRegistry; + this.dictionaryService = serviceRegistry.getDictionaryService(); + } + + /** + * Set policy component + * + * @param policyComponent policy component + */ + public void setPolicyComponent(PolicyComponent policyComponent) + { + this.policyComponent = policyComponent; + } + + /** + * Set search service + * + * @param nodeService search service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Set the node DAO object + * + * @param nodeDAO node DAO + */ + public void setNodeDAO(NodeDAO nodeDAO) + { + this.nodeDAO = nodeDAO; + } + + /** + * Set records management action service + * + * @param rmActionService records management action service + */ + public void setRmActionService(RecordsManagementActionService rmActionService) + { + this.rmActionService = rmActionService; + } + + /** + * Sets the default RM store reference + * @param defaultStoreRef store reference + */ + @Deprecated + public void setDefaultStoreRef(StoreRef defaultStoreRef) + { + this.defaultStoreRef = defaultStoreRef; + } + + /** + * Init method. Registered behaviours. + */ + public void init() + { + // Register the association behaviours + this.policyComponent.bindAssociationBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onCreateChildAssociation"), + TYPE_RECORD_FOLDER, + ContentModel.ASSOC_CONTAINS, + new JavaBehaviour(this, "onFileContent", NotificationFrequency.TRANSACTION_COMMIT)); + + this.policyComponent.bindAssociationBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onCreateChildAssociation"), + TYPE_RECORD_CATEGORY, + ContentModel.ASSOC_CONTAINS, + new JavaBehaviour(this, "onAddContentToContainer", NotificationFrequency.EVERY_EVENT)); + + // Register script execution behaviour on RM property update. + this.policyComponent.bindClassBehaviour(QName.createQName(NamespaceService.ALFRESCO_URI, "onUpdateProperties"), + ASPECT_FILE_PLAN_COMPONENT, + new JavaBehaviour(this, "onChangeToAnyRmProperty", NotificationFrequency.TRANSACTION_COMMIT)); + + // Disposition behaviours + onChangeToDispositionActionDefinition = new JavaBehaviour(this, "onChangeToDispositionActionDefinition", NotificationFrequency.TRANSACTION_COMMIT); + this.policyComponent.bindClassBehaviour(QName.createQName(NamespaceService.ALFRESCO_URI, "onUpdateProperties"), + TYPE_DISPOSITION_ACTION_DEFINITION, + onChangeToDispositionActionDefinition); + + // Reference behaviours + policyComponent.bindClassBehaviour(RecordsManagementPolicies.ON_CREATE_REFERENCE, + ASPECT_RECORD, + new JavaBehaviour(this, "onCreateReference", NotificationFrequency.TRANSACTION_COMMIT)); + + policyComponent.bindClassBehaviour(RecordsManagementPolicies.ON_REMOVE_REFERENCE, + ASPECT_RECORD, + new JavaBehaviour(this, "onRemoveReference", NotificationFrequency.TRANSACTION_COMMIT)); + + // Identifier behaviours + policyComponent.bindClassBehaviour(QName.createQName(NamespaceService.ALFRESCO_URI, "onUpdateProperties"), + ASPECT_RECORD_COMPONENT_ID, + new JavaBehaviour(this, "onIdentifierUpdate", NotificationFrequency.TRANSACTION_COMMIT)); + } + + /** + * Try to file any record created in a record folder + * + * @see org.alfresco.repo.node.NodeServicePolicies.OnCreateChildAssociationPolicy#onCreateChildAssociation(org.alfresco.service.cmr.repository.ChildAssociationRef, boolean) + */ + public void onFileContent(ChildAssociationRef childAssocRef, boolean bNew) + { + // File the document + rmActionService.executeRecordsManagementAction(childAssocRef.getChildRef(), "file"); + } + + /** + * On add content to container + * + * Prevents content nodes being added to record series and record category folders + * by imap, cifs etc. + * + * @param childAssocRef + * @param bNew + */ + public void onAddContentToContainer(ChildAssociationRef childAssocRef, boolean bNew) + { + if (childAssocRef.getTypeQName().equals(ContentModel.ASSOC_CONTAINS)) + { + QName childType = nodeService.getType(childAssocRef.getChildRef()); + + if(childType.equals(ContentModel.TYPE_CONTENT)) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_ERROR_ADD_CONTENT_CONTAINER)); + } + } + } + + /** + * Called after a DispositionActionDefinition property has been updated. + */ + public void onChangeToDispositionActionDefinition(NodeRef node, Map oldProps, Map newProps) + { + if (nodeService.exists(node) == true) + { + onChangeToDispositionActionDefinition.disable(); + try + { + // Determine the properties that have changed + Set changedProps = this.determineChangedProps(oldProps, newProps); + + if (nodeService.hasAspect(node, ASPECT_UNPUBLISHED_UPDATE) == false) + { + // Apply the unpublished aspect + Map props = new HashMap(); + props.put(PROP_UPDATE_TO, UPDATE_TO_DISPOSITION_ACTION_DEFINITION); + props.put(PROP_UPDATED_PROPERTIES, (Serializable)changedProps); + nodeService.addAspect(node, ASPECT_UNPUBLISHED_UPDATE, props); + } + else + { + Map props = nodeService.getProperties(node); + + // Check that there isn't a update currently being published + if ((Boolean)props.get(PROP_PUBLISH_IN_PROGRESS).equals(Boolean.TRUE) == true) + { + // Can not update the disposition schedule since there is an outstanding update being published + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_UPDATE_DISP_ACT_DEF)); + } + + // Update the update information + props.put(PROP_UPDATE_TO, UPDATE_TO_DISPOSITION_ACTION_DEFINITION); + props.put(PROP_UPDATED_PROPERTIES, (Serializable)changedProps); + nodeService.setProperties(node, props); + } + } + finally + { + onChangeToDispositionActionDefinition.enable(); + } + } + } + + /** + * Called after any Records Management property has been updated. + */ + public void onChangeToAnyRmProperty(NodeRef node, Map oldProps, Map newProps) + { + if (nodeService.exists(node) == true) + { + this.lookupAndExecuteScripts(node, oldProps, newProps); + } + } + + /** + * Property update behaviour implementation + * + * @param node + * @param oldProps + * @param newProps + */ + public void onIdentifierUpdate(NodeRef node, Map oldProps, Map newProps) + { + if (nodeService.exists(node) == true) + { + String newIdValue = (String)newProps.get(PROP_IDENTIFIER); + if (newIdValue != null) + { + String oldIdValue = (String)oldProps.get(PROP_IDENTIFIER); + if (oldIdValue != null && oldIdValue.equals(newIdValue) == false) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_SET_ID, node.toString())); + } + } + } + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementPolicies.OnCreateReference#onCreateReference(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName) + */ + public void onCreateReference(NodeRef fromNodeRef, NodeRef toNodeRef, QName reference) + { + // Deal with versioned records + if (reference.equals(QName.createQName(RecordsManagementCustomModel.RM_CUSTOM_URI, "versions")) == true) + { + // Apply the versioned aspect to the from node + this.nodeService.addAspect(fromNodeRef, ASPECT_VERSIONED_RECORD, null); + } + + // Execute script if for the reference event + executeReferenceScript("onCreate", reference, fromNodeRef, toNodeRef); + } + + /** + * Executes a reference script if present + * + * @param policy + * @param reference + * @param from + * @param to + */ + private void executeReferenceScript(String policy, QName reference, NodeRef from, NodeRef to) + { + String referenceId = reference.getLocalName(); + + // This is the filename pattern which is assumed. + // e.g. a script file onCreate_superceded.js for the creation of a superseded reference + String expectedScriptName = policy + "_" + referenceId + ".js"; + + NodeRef scriptNodeRef = nodeService.getChildByName(scriptsFolderNodeRef, ContentModel.ASSOC_CONTAINS, expectedScriptName); + if (scriptNodeRef != null) + { + Map objectModel = new HashMap(1); + objectModel.put("node", from); + objectModel.put("toNode", to); + objectModel.put("policy", policy); + objectModel.put("reference", referenceId); + + serviceRegistry.getScriptService().executeScript(scriptNodeRef, null, objectModel); + } + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementPolicies.OnRemoveReference#onRemoveReference(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName) + */ + public void onRemoveReference(NodeRef fromNodeRef, NodeRef toNodeRef, QName reference) + { + // Deal with versioned records + if (reference.equals(QName.createQName(RecordsManagementCustomModel.RM_CUSTOM_URI, "versions")) == true) + { + // Apply the versioned aspect to the from node + this.nodeService.removeAspect(fromNodeRef, ASPECT_VERSIONED_RECORD); + } + + // Execute script if for the reference event + executeReferenceScript("onRemove", reference, fromNodeRef, toNodeRef); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementService#isFilePlan(org.alfresco.service.cmr.repository.NodeRef) + */ + public boolean isFilePlan(NodeRef nodeRef) + { + return instanceOf(nodeRef, TYPE_FILE_PLAN); + } + + /** + * Utility method to safely and quickly determine if a node is a type (or sub-type) of the one specified. + */ + private boolean instanceOf(NodeRef nodeRef, QName ofClassName) + { + ParameterCheck.mandatory("nodeRef", nodeRef); + ParameterCheck.mandatory("ofClassName", ofClassName); + boolean result = false; + if (nodeService.exists(nodeRef) == true && + (ofClassName.equals(nodeService.getType(nodeRef)) == true || + dictionaryService.isSubClass(nodeService.getType(nodeRef), ofClassName) == true)) + { + result = true; + } + return result; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementService#isFilePlanComponent(org.alfresco.service.cmr.repository.NodeRef) + */ + public boolean isFilePlanComponent(NodeRef nodeRef) + { + boolean result = false; + if (nodeService.exists(nodeRef) == true && + nodeService.hasAspect(nodeRef, ASPECT_FILE_PLAN_COMPONENT) == true) + { + result = true; + } + return result; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementService#getFilePlanComponentKind(org.alfresco.service.cmr.repository.NodeRef) + */ + public FilePlanComponentKind getFilePlanComponentKind(NodeRef nodeRef) + { + FilePlanComponentKind result = null; + + if (isFilePlanComponent(nodeRef) == true) + { + result = FilePlanComponentKind.FILE_PLAN_COMPONENT; + + if (isFilePlan(nodeRef) == true) + { + result = FilePlanComponentKind.FILE_PLAN; + } + else if (isRecordCategory(nodeRef) == true) + { + result = FilePlanComponentKind.RECORD_CATEGORY; + } + else if (isRecordFolder(nodeRef) == true) + { + result = FilePlanComponentKind.RECORD_FOLDER; + } + else if (isRecord(nodeRef) == true) + { + result = FilePlanComponentKind.RECORD; + } + else if (isHold(nodeRef) == true) + { + result = FilePlanComponentKind.HOLD; + } + else if (isTransfer(nodeRef) == true) + { + result = FilePlanComponentKind.TRANSFER; + } + else if (instanceOf(nodeRef, TYPE_DISPOSITION_SCHEDULE) == true || instanceOf(nodeRef, TYPE_DISPOSITION_ACTION_DEFINITION) == true) + { + result = FilePlanComponentKind.DISPOSITION_SCHEDULE; + } + } + + return result; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementService#isRecordCategory(org.alfresco.service.cmr.repository.NodeRef) + */ + public boolean isRecordCategory(NodeRef nodeRef) + { + return instanceOf(nodeRef, TYPE_RECORD_CATEGORY); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementService#isRecordFolder(org.alfresco.service.cmr.repository.NodeRef) + */ + public boolean isRecordFolder(NodeRef nodeRef) + { + return instanceOf(nodeRef, TYPE_RECORD_FOLDER); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementService#isRecord(org.alfresco.service.cmr.repository.NodeRef) + */ + public boolean isRecord(NodeRef nodeRef) + { + return this.nodeService.hasAspect(nodeRef, ASPECT_RECORD); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementService#isHold(org.alfresco.service.cmr.repository.NodeRef) + */ + public boolean isHold(NodeRef nodeRef) + { + return instanceOf(nodeRef, TYPE_HOLD); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementService#isTransfer(org.alfresco.service.cmr.repository.NodeRef) + */ + public boolean isTransfer(NodeRef nodeRef) + { + return instanceOf(nodeRef, TYPE_TRANSFER); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementService#isMetadataStub(org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + public boolean isMetadataStub(NodeRef nodeRef) + { + return nodeService.hasAspect(nodeRef, ASPECT_GHOSTED); + } + + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementService#isFrozen(org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + public boolean isFrozen(NodeRef nodeRef) + { + return nodeService.hasAspect(nodeRef, ASPECT_FROZEN); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementService#hasFrozenChildren(org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + public boolean hasFrozenChildren(NodeRef nodeRef) + { + boolean result = false; + if (isFilePlanComponent(nodeRef) == true) + { + List assocs = nodeService.getChildAssocs(nodeRef, ContentModel.ASSOC_CONTAINS, RegexQNamePattern.MATCH_ALL); + for (ChildAssociationRef assoc : assocs) + { + if (isFrozen(assoc.getChildRef()) == true) + { + result = true; + break; + } + } + } + return result; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementService#isCutoff(org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + public boolean isCutoff(NodeRef nodeRef) + { + return nodeService.hasAspect(nodeRef, ASPECT_CUT_OFF); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementService#getFilePlan(org.alfresco.service.cmr.repository.NodeRef) + */ + public NodeRef getFilePlan(NodeRef nodeRef) + { + NodeRef result = null; + + if (nodeRef != null) + { + result = (NodeRef)nodeService.getProperty(nodeRef, PROP_ROOT_NODEREF); + if (result == null) + { + if (instanceOf(nodeRef, TYPE_FILE_PLAN) == true) + { + result = nodeRef; + } + else + { + ChildAssociationRef parentAssocRef = nodeService.getPrimaryParent(nodeRef); + if (parentAssocRef != null) + { + result = getFilePlan(parentAssocRef.getParentRef()); + } + } + } + } + + return result; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementService#getNodeRefPath(org.alfresco.service.cmr.repository.NodeRef) + */ + public List getNodeRefPath(NodeRef nodeRef) + { + LinkedList nodeRefPath = new LinkedList(); + try + { + getNodeRefPathRecursive(nodeRef, nodeRefPath); + } + catch (Throwable e) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_PATH_NODE, nodeRef), e); + } + return nodeRefPath; + } + + /** + * Helper method to build a NodeRef path from the node to the RM root + */ + private void getNodeRefPathRecursive(NodeRef nodeRef, LinkedList nodeRefPath) + { + if (isFilePlanComponent(nodeRef) == false) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_INVALID_RM_NODE, ASPECT_FILE_PLAN_COMPONENT.toString())); + } + // Prepend it to the path + nodeRefPath.addFirst(nodeRef); + // Are we at the root + if (isFilePlan(nodeRef) == true) + { + // We're done + } + else + { + ChildAssociationRef assocRef = nodeService.getPrimaryParent(nodeRef); + if (assocRef == null) + { + // We hit the top of the store + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_NO_ROOT)); + } + // Recurse + nodeRef = assocRef.getParentRef(); + getNodeRefPathRecursive(nodeRef, nodeRefPath); + } + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementService#getRecordsManagementRoots(org.alfresco.service.cmr.repository.StoreRef) + */ + public List getFilePlans() + { + final List results = new ArrayList(); + Set aspects = new HashSet(1); + aspects.add(ASPECT_RECORDS_MANAGEMENT_ROOT); + nodeDAO.getNodesWithAspects(aspects, Long.MIN_VALUE, Long.MAX_VALUE, new NodeDAO.NodeRefQueryCallback() + { + @Override + public boolean handle(Pair nodePair) + { + results.add(nodePair.getSecond()); + return true; + } + }); + return results; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementService#createFilePlan(org.alfresco.service.cmr.repository.NodeRef, java.lang.String, org.alfresco.service.namespace.QName, java.util.Map) + */ + public NodeRef createFilePlan(NodeRef parent, String name, QName type, Map properties) + { + ParameterCheck.mandatory("parent", parent); + ParameterCheck.mandatory("name", name); + ParameterCheck.mandatory("type", type); + + // Check the parent is not already an RM component node + // ie: you can't create a rm root in an existing rm hierarchy + if (isFilePlanComponent(parent) == true) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_DUP_ROOT)); + } + + // Check that the passed type is a sub-type of rma:filePlan + if (TYPE_FILE_PLAN.equals(type) == false && + dictionaryService.isSubClass(type, TYPE_FILE_PLAN) == false) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_ROOT_TYPE, type.toString())); + } + + // Build map of properties + Map rmRootProps = new HashMap(1); + if (properties != null && properties.size() != 0) + { + rmRootProps.putAll(properties); + } + rmRootProps.put(ContentModel.PROP_NAME, name); + + // Create the root + ChildAssociationRef assocRef = nodeService.createNode( + parent, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, name), + type, + rmRootProps); + + // TODO do we need to create role and security groups or is this done automatically? + + return assocRef.getChildRef(); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementService#createFilePlan(org.alfresco.service.cmr.repository.NodeRef, java.lang.String, java.util.Map) + */ + public NodeRef createFilePlan(NodeRef parent, String name, Map properties) + { + return createFilePlan(parent, name, TYPE_FILE_PLAN, properties); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementService#createFilePlan(org.alfresco.service.cmr.repository.NodeRef, java.lang.String) + */ + public NodeRef createFilePlan(NodeRef parent, String name) + { + return createFilePlan(parent, name, TYPE_FILE_PLAN, null); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementService#createFilePlan(org.alfresco.service.cmr.repository.NodeRef, java.lang.String, org.alfresco.service.namespace.QName) + */ + @Override + public NodeRef createFilePlan(NodeRef parent, String name, QName type) + { + return createFilePlan(parent, name, type, null); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementService#createRecordCategory(org.alfresco.service.cmr.repository.NodeRef, java.lang.String, org.alfresco.service.namespace.QName, java.util.Map) + */ + public NodeRef createRecordCategory(NodeRef parent, String name, QName type, Map properties) + { + ParameterCheck.mandatory("parent", parent); + ParameterCheck.mandatory("name", name); + ParameterCheck.mandatory("type", type); + + // Check that the parent is a container + QName parentType = nodeService.getType(parent); + if (TYPE_RECORDS_MANAGEMENT_CONTAINER.equals(parentType) == false && + dictionaryService.isSubClass(parentType, TYPE_RECORDS_MANAGEMENT_CONTAINER) == false) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_CONTAINER_PARENT_TYPE, parentType.toString())); + } + + // Check that the the provided type is a sub-type of rm:recordCategory + if (TYPE_RECORD_CATEGORY.equals(type) == false && + dictionaryService.isSubClass(type, TYPE_RECORD_CATEGORY) == false) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_CONTAINER_TYPE, type.toString())); + } + + // Set the properties for the record category + Map props = new HashMap(1); + if (properties != null && properties.size() != 0) + { + props.putAll(properties); + } + props.put(ContentModel.PROP_NAME, name); + + return nodeService.createNode( + parent, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, name), + type, + props).getChildRef(); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementService#createRecordCategory(org.alfresco.service.cmr.repository.NodeRef, java.lang.String) + */ + public NodeRef createRecordCategory(NodeRef parent, String name) + { + return createRecordCategory(parent, name, TYPE_RECORD_CATEGORY); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementService#createRecordCategory(org.alfresco.service.cmr.repository.NodeRef, java.lang.String, java.util.Map) + */ + public NodeRef createRecordCategory(NodeRef parent, String name, Map properties) + { + return createRecordCategory(parent, name, TYPE_RECORD_CATEGORY, properties); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementService#createRecordCategory(org.alfresco.service.cmr.repository.NodeRef, java.lang.String, org.alfresco.service.namespace.QName) + */ + public NodeRef createRecordCategory(NodeRef parent, String name, QName type) + { + return createRecordCategory(parent, name, type, null); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementService#getAllContained(org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + public List getAllContained(NodeRef container) + { + return getAllContained(container, false); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementService#getAllContained(org.alfresco.service.cmr.repository.NodeRef, boolean) + */ + @Override + public List getAllContained(NodeRef container, boolean deep) + { + return getContained(container, null, deep); + } + + /** + * Get contained nodes of a particular type. If null return all. + * + * @param container container node reference + * @param typeFilter type filter, null if none + * @return {@link List}<{@link NodeRef> list of contained node references + */ + private List getContained(NodeRef container, QName typeFilter, boolean deep) + { + // Parameter check + ParameterCheck.mandatory("container", container); + + // Check we have a container in our hands + if (isRecordCategory(container) == false) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_CONTAINER_EXPECTED)); + } + + List result = new ArrayList(1); + List assocs = this.nodeService.getChildAssocs(container, ContentModel.ASSOC_CONTAINS, RegexQNamePattern.MATCH_ALL); + for (ChildAssociationRef assoc : assocs) + { + NodeRef child = assoc.getChildRef(); + QName childType = nodeService.getType(child); + if (typeFilter == null || + typeFilter.equals(childType) == true || + dictionaryService.isSubClass(childType, typeFilter) == true) + { + result.add(child); + } + + // Inspect the containers and add children if deep + if (deep == true && + (TYPE_RECORD_CATEGORY.equals(childType) == true || + dictionaryService.isSubClass(childType, TYPE_RECORD_CATEGORY) == true)) + { + result.addAll(getContained(child, typeFilter, deep)); + } + } + + return result; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementService#getContainedRecordCategories(org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + public List getContainedRecordCategories(NodeRef container) + { + return getContainedRecordCategories(container, false); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementService#getContainedRecordCategories(org.alfresco.service.cmr.repository.NodeRef, boolean) + */ + @Override + public List getContainedRecordCategories(NodeRef container, boolean deep) + { + return getContained(container, TYPE_RECORD_CATEGORY, deep); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementService#getContainedRecordFolders(org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + public List getContainedRecordFolders(NodeRef container) + { + return getContainedRecordFolders(container, false); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementService#getContainedRecordFolders(org.alfresco.service.cmr.repository.NodeRef, boolean) + */ + @Override + public List getContainedRecordFolders(NodeRef container, boolean deep) + { + return getContained(container, TYPE_RECORD_FOLDER, deep); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementService#isRecordFolderDeclared(org.alfresco.service.cmr.repository.NodeRef) + */ + public boolean isRecordFolderDeclared(NodeRef recordFolder) + { + // Check we have a record folder + if (isRecordFolder(recordFolder) == false) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_RECORD_FOLDER_EXPECTED)); + } + + boolean result = true; + + // Check that each record in the record folder in declared + List records = getRecords(recordFolder); + for (NodeRef record : records) + { + if (isRecordDeclared(record) == false) + { + result = false; + break; + } + } + + return result; + + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementService#isRecordFolderClosed(org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + public boolean isRecordFolderClosed(NodeRef nodeRef) + { + // Check we have a record folder + if (isRecordFolder(nodeRef) == false) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_RECORD_FOLDER_EXPECTED)); + } + + return ((Boolean)this.nodeService.getProperty(nodeRef, PROP_IS_CLOSED)).booleanValue(); + } + + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementService#getRecordFolders(org.alfresco.service.cmr.repository.NodeRef) + */ + public List getRecordFolders(NodeRef record) + { + List result = new ArrayList(1); + if (isRecord(record) == true) + { + List assocs = this.nodeService.getParentAssocs(record, ContentModel.ASSOC_CONTAINS, RegexQNamePattern.MATCH_ALL); + for (ChildAssociationRef assoc : assocs) + { + NodeRef parent = assoc.getParentRef(); + if (isRecordFolder(parent) == true) + { + result.add(parent); + } + } + } + return result; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementService#createRecordFolder(org.alfresco.service.cmr.repository.NodeRef, java.lang.String, org.alfresco.service.namespace.QName, java.util.Map) + */ + public NodeRef createRecordFolder(NodeRef rmContainer, String name, QName type, Map properties) + { + ParameterCheck.mandatory("rmContainer", rmContainer); + ParameterCheck.mandatory("name", name); + ParameterCheck.mandatory("type", type); + + // Check that we are not trying to create a record folder in a root container + if (isFilePlan(rmContainer) == true) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_PARENT_RECORD_FOLDER_ROOT)); + } + + // Check that the parent is a container + QName parentType = nodeService.getType(rmContainer); + if (TYPE_RECORD_CATEGORY.equals(parentType) == false && + dictionaryService.isSubClass(parentType, TYPE_RECORD_CATEGORY) == false) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_PARENT_RECORD_FOLDER_TYPE, parentType.toString())); + } + + // Check that the the provided type is a sub-type of rm:recordFolder + if (TYPE_RECORD_FOLDER.equals(type) == false && + dictionaryService.isSubClass(type, TYPE_RECORD_FOLDER) == false) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_RECORD_FOLDER_TYPE, type.toString())); + } + + Map props = new HashMap(1); + if (properties != null && properties.size() != 0) + { + props.putAll(properties); + } + props.put(ContentModel.PROP_NAME, name); + + return nodeService.createNode( + rmContainer, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, name), + type, + props).getChildRef(); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementService#createRecordFolder(org.alfresco.service.cmr.repository.NodeRef, java.lang.String) + */ + public NodeRef createRecordFolder(NodeRef rmContrainer, String name) + { + // TODO defaults to rm:recordFolder, but in future could auto-detect sub-type of folder based on + // context + return createRecordFolder(rmContrainer, name, TYPE_RECORD_FOLDER); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementService#createRecordFolder(org.alfresco.service.cmr.repository.NodeRef, java.lang.String, java.util.Map) + */ + public NodeRef createRecordFolder(NodeRef parent, String name, Map properties) + { + return createRecordFolder(parent, name, TYPE_RECORD_FOLDER, properties); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementService#createRecordFolder(org.alfresco.service.cmr.repository.NodeRef, java.lang.String, org.alfresco.service.namespace.QName) + */ + public NodeRef createRecordFolder(NodeRef parent, String name, QName type) + { + return createRecordFolder(parent, name, type, null); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementService#getRecordMetaDataAspects() + */ + public Set getRecordMetaDataAspects() + { + if (recordMetaDataAspects == null) + { + recordMetaDataAspects = new HashSet(7); + Collection aspects = dictionaryService.getAllAspects(); + for (QName aspect : aspects) + { + AspectDefinition def = dictionaryService.getAspect(aspect); + if (def != null) + { + QName parent = def.getParentName(); + if (parent != null && ASPECT_RECORD_META_DATA.equals(parent) == true) + { + recordMetaDataAspects.add(aspect); + } + } + } + } + return recordMetaDataAspects; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementService#getRecords(org.alfresco.service.cmr.repository.NodeRef) + */ + public List getRecords(NodeRef recordFolder) + { + List result = new ArrayList(1); + if (isRecordFolder(recordFolder) == true) + { + List assocs = this.nodeService.getChildAssocs(recordFolder, ContentModel.ASSOC_CONTAINS, RegexQNamePattern.MATCH_ALL); + for (ChildAssociationRef assoc : assocs) + { + NodeRef child = assoc.getChildRef(); + if (isRecord(child) == true) + { + result.add(child); + } + } + } + return result; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementService#isRecord(org.alfresco.service.cmr.repository.NodeRef, boolean) + */ + public boolean isRecordDeclared(NodeRef record) + { + if (isRecord(record) == false) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_NOT_RECORD, record.toString())); + } + return (this.nodeService.hasAspect(record, ASPECT_DECLARED_RECORD)); + } + + + + /** + * This method examines the old and new property sets and for those properties which + * have changed, looks for script resources corresponding to those properties. + * Those scripts are then called via the ScriptService. + * + * @param nodeWithChangedProperties the node whose properties have changed. + * @param oldProps the old properties and their values. + * @param newProps the new properties and their values. + * + * @see #lookupScripts(Map, Map) + */ + private void lookupAndExecuteScripts(NodeRef nodeWithChangedProperties, + Map oldProps, Map newProps) + { + List scriptRefs = lookupScripts(oldProps, newProps); + + Map objectModel = new HashMap(1); + objectModel.put("node", nodeWithChangedProperties); + objectModel.put("oldProperties", oldProps); + objectModel.put("newProperties", newProps); + + for (NodeRef scriptRef : scriptRefs) + { + serviceRegistry.getScriptService().executeScript(scriptRef, null, objectModel); + } + } + + /** + * This method determines which properties have changed and for each such property + * looks for a script resource in a well-known location. + * + * @param oldProps the old properties and their values. + * @param newProps the new properties and their values. + * @return A list of nodeRefs corresponding to the Script resources. + * + * @see #determineChangedProps(Map, Map) + */ + private List lookupScripts(Map oldProps, Map newProps) + { + List result = new ArrayList(); + + Set changedProps = determineChangedProps(oldProps, newProps); + for (QName propQName : changedProps) + { + QName prefixedQName = propQName.getPrefixedQName(serviceRegistry.getNamespaceService()); + + String [] splitQName = QName.splitPrefixedQName(prefixedQName.toPrefixString()); + final String shortPrefix = splitQName[0]; + final String localName = splitQName[1]; + + // This is the filename pattern which is assumed. + // e.g. a script file cm_name.js would be called for changed to cm:name + String expectedScriptName = shortPrefix + "_" + localName + ".js"; + + NodeRef nextElement = nodeService.getChildByName(scriptsFolderNodeRef, ContentModel.ASSOC_CONTAINS, expectedScriptName); + if (nextElement != null) result.add(nextElement); + } + + return result; + } + + /** + * This method compares the oldProps map against the newProps map and returns + * a set of QNames of the properties that have changed. Changed here means one of + *
    + *
  • the property has been removed
  • + *
  • the property has had its value changed
  • + *
  • the property has been added
  • + *
+ */ + private Set determineChangedProps(Map oldProps, Map newProps) + { + Set result = new HashSet(); + for (QName qn : oldProps.keySet()) + { + if (newProps.get(qn) == null || + newProps.get(qn).equals(oldProps.get(qn)) == false) + { + result.add(qn); + } + } + for (QName qn : newProps.keySet()) + { + if (oldProps.get(qn) == null) + { + result.add(qn); + } + } + + return result; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/RecordsManagementServiceRegistry.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/RecordsManagementServiceRegistry.java new file mode 100644 index 0000000000..bca1f20bcd --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/RecordsManagementServiceRegistry.java @@ -0,0 +1,88 @@ +/* + * 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.module.org_alfresco_module_rm; + +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService; +import org.alfresco.module.org_alfresco_module_rm.audit.RecordsManagementAuditService; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService; +import org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEventService; +import org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService; +import org.alfresco.service.NotAuditable; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; + +/** + * Records management service registry + * + * @author Roy Wetherall + */ +public interface RecordsManagementServiceRegistry extends ServiceRegistry +{ + /** Service QName constants */ + static final QName RECORDS_MANAGEMENT_SERVICE = QName.createQName(NamespaceService.ALFRESCO_URI, "RecordsManagementService"); + static final QName DISPOSITION_SERVICE = QName.createQName(NamespaceService.ALFRESCO_URI, "DispositionService"); + static final QName RECORDS_MANAGEMENT_ADMIN_SERVICE = QName.createQName(NamespaceService.ALFRESCO_URI, "RecordsManagementAdminService"); + static final QName RECORDS_MANAGEMENT_ACTION_SERVICE = QName.createQName(NamespaceService.ALFRESCO_URI, "RecordsManagementActionService"); + static final QName RECORDS_MANAGEMENT_EVENT_SERVICE = QName.createQName(NamespaceService.ALFRESCO_URI, "RecordsManagementEventService"); + static final QName RECORDS_MANAGEMENT_SECURITY_SERVICE = QName.createQName(NamespaceService.ALFRESCO_URI, "RecordsManagementSecurityService"); + static final QName RECORDS_MANAGEMENT_AUDIT_SERVICE = QName.createQName(NamespaceService.ALFRESCO_URI, "RecordsManagementAuditService"); + + /** + * @return records management service + */ + @NotAuditable + RecordsManagementService getRecordsManagementService(); + + /** + * @return disposition service + */ + @NotAuditable + DispositionService getDispositionService(); + + /** + * @return records management admin service + */ + @NotAuditable + RecordsManagementAdminService getRecordsManagementAdminService(); + + /** + * @return records management action service + */ + @NotAuditable + RecordsManagementActionService getRecordsManagementActionService(); + + /** + * @return records management event service + */ + @NotAuditable + RecordsManagementEventService getRecordsManagementEventService(); + + /** + * @return records management security service + */ + @NotAuditable + RecordsManagementSecurityService getRecordsManagementSecurityService(); + + /** + * @return records management audit service + */ + @NotAuditable + RecordsManagementAuditService getRecordsManagementAuditService(); +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/RecordsManagementServiceRegistryImpl.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/RecordsManagementServiceRegistryImpl.java new file mode 100644 index 0000000000..253fd248a3 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/RecordsManagementServiceRegistryImpl.java @@ -0,0 +1,92 @@ +/* + * 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.module.org_alfresco_module_rm; + +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService; +import org.alfresco.module.org_alfresco_module_rm.audit.RecordsManagementAuditService; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService; +import org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEventService; +import org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService; +import org.alfresco.repo.service.ServiceDescriptorRegistry; + +/** + * Records management service registry implementation + * + * @author Roy Wetherall + */ +public class RecordsManagementServiceRegistryImpl extends ServiceDescriptorRegistry + implements RecordsManagementServiceRegistry +{ + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementServiceRegistry#getRecordsManagementActionService() + */ + public RecordsManagementActionService getRecordsManagementActionService() + { + return (RecordsManagementActionService)getService(RECORDS_MANAGEMENT_ACTION_SERVICE); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementServiceRegistry#getRecordsManagementAdminService() + */ + public RecordsManagementAdminService getRecordsManagementAdminService() + { + return (RecordsManagementAdminService)getService(RECORDS_MANAGEMENT_ADMIN_SERVICE); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementServiceRegistry#getRecordsManagementEventService() + */ + public RecordsManagementEventService getRecordsManagementEventService() + { + return (RecordsManagementEventService)getService(RECORDS_MANAGEMENT_EVENT_SERVICE); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementServiceRegistry#getRecordsManagementService() + */ + public RecordsManagementService getRecordsManagementService() + { + return (RecordsManagementService)getService(RECORDS_MANAGEMENT_SERVICE); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementServiceRegistry#getRecordsManagementSecurityService() + */ + public RecordsManagementSecurityService getRecordsManagementSecurityService() + { + return (RecordsManagementSecurityService)getService(RECORDS_MANAGEMENT_SECURITY_SERVICE); + } + + /* + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementServiceRegistry#getRecordsManagementAuditService() + */ + public RecordsManagementAuditService getRecordsManagementAuditService() + { + return (RecordsManagementAuditService)getService(RECORDS_MANAGEMENT_AUDIT_SERVICE); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementServiceRegistry#getDictionaryService() + */ + @Override + public DispositionService getDispositionService() + { + return (DispositionService)getService(DISPOSITION_SERVICE); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/RMActionExecuterAbstractBase.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/RMActionExecuterAbstractBase.java new file mode 100644 index 0000000000..92543eaf4b --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/RMActionExecuterAbstractBase.java @@ -0,0 +1,609 @@ +/* + * 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.module.org_alfresco_module_rm.action; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminService; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.audit.RecordsManagementAuditService; +import org.alfresco.module.org_alfresco_module_rm.capability.impl.AbstractCapability; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionAction; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionActionDefinition; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService; +import org.alfresco.module.org_alfresco_module_rm.event.EventCompletionDetails; +import org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEvent; +import org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEventService; +import org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEventType; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.module.org_alfresco_module_rm.vital.VitalRecordService; +import org.alfresco.repo.action.executer.ActionExecuterAbstractBase; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ActionService; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.Period; +import org.alfresco.service.cmr.security.OwnableService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.PropertyCheck; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.extensions.surf.util.I18NUtil; +import org.springframework.util.StringUtils; + +/** + * Records management action executer base class + * + * @author Roy Wetherall + */ +public abstract class RMActionExecuterAbstractBase extends ActionExecuterAbstractBase + implements RecordsManagementAction, + RecordsManagementModel, + BeanNameAware +{ + /** Namespace service */ + protected NamespaceService namespaceService; + + /** Used to control transactional behaviour including post-commit auditing */ + protected TransactionService transactionService; + + /** Node service */ + protected NodeService nodeService; + + /** Dictionary service */ + protected DictionaryService dictionaryService; + + /** Content service */ + protected ContentService contentService; + + /** Action service */ + protected ActionService actionService; + + /** Records management action service */ + protected RecordsManagementAuditService recordsManagementAuditService; + + /** Records management action service */ + protected RecordsManagementActionService recordsManagementActionService; + + /** Records management service */ + protected RecordsManagementService recordsManagementService; + + /** Disposition service */ + protected DispositionService dispositionService; + + /** Vital record service */ + protected VitalRecordService vitalRecordService; + + /** Records management event service */ + protected RecordsManagementEventService recordsManagementEventService; + + /** Records management action service */ + protected RecordsManagementAdminService recordsManagementAdminService; + + /** Ownable service **/ + protected OwnableService ownableService; + + protected LinkedList capabilities = new LinkedList();; + + /** Default constructor */ + public RMActionExecuterAbstractBase() + { + } + + /** + * Set the namespace service + */ + public void setTransactionService(TransactionService transactionService) + { + this.transactionService = transactionService; + } + + /** + * Set the namespace service + */ + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + /** + * Set node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Set the dictionary service + */ + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + /** + * Set the content service + */ + public void setContentService(ContentService contentService) + { + this.contentService = contentService; + } + + /** + * Set action service + */ + public void setActionService(ActionService actionService) + { + this.actionService = actionService; + } + + /** + * Set the audit service that action details will be sent to + */ + public void setRecordsManagementAuditService(RecordsManagementAuditService recordsManagementAuditService) + { + this.recordsManagementAuditService = recordsManagementAuditService; + } + + /** + * Set records management service + */ + public void setRecordsManagementActionService(RecordsManagementActionService recordsManagementActionService) + { + this.recordsManagementActionService = recordsManagementActionService; + } + + /** + * Set records management service + */ + public void setRecordsManagementService(RecordsManagementService recordsManagementService) + { + this.recordsManagementService = recordsManagementService; + } + + /** + * Set the disposition service + */ + public void setDispositionService(DispositionService dispositionService) + { + this.dispositionService = dispositionService; + } + + /** + * @param vitalRecordService vital record service + */ + public void setVitalRecordService(VitalRecordService vitalRecordService) + { + this.vitalRecordService = vitalRecordService; + } + + /** + * Set records management event service + */ + public void setRecordsManagementEventService(RecordsManagementEventService recordsManagementEventService) + { + this.recordsManagementEventService = recordsManagementEventService; + } + + + /** + * Set the ownable service + * @param ownableSerice + */ + public void setOwnableService(OwnableService ownableService) + { + this.ownableService = ownableService; + } + + /** + * Register with a single capability + * @param capability + */ + public void setCapability(AbstractCapability capability) + { + capabilities.add(capability); + } + + /** + * Register with several capabilities + * @param capabilities + */ + public void setCapabilities(Collection capabilities) + { + this.capabilities.addAll(capabilities); + } + + public void setRecordsManagementAdminService(RecordsManagementAdminService recordsManagementAdminService) + { + this.recordsManagementAdminService = recordsManagementAdminService; + } + + public RecordsManagementAdminService getRecordsManagementAdminService() + { + return recordsManagementAdminService; + } + + /** + * Init method + */ + @Override + public void init() + { + PropertyCheck.mandatory(this, "namespaceService", namespaceService); + PropertyCheck.mandatory(this, "transactionService", transactionService); + PropertyCheck.mandatory(this, "nodeService", nodeService); + PropertyCheck.mandatory(this, "dictionaryService", dictionaryService); + PropertyCheck.mandatory(this, "contentService", contentService); + PropertyCheck.mandatory(this, "actionService", actionService); + PropertyCheck.mandatory(this, "transactionService", transactionService); + PropertyCheck.mandatory(this, "recordsManagementAuditService", recordsManagementAuditService); + PropertyCheck.mandatory(this, "recordsManagementActionService", recordsManagementActionService); + PropertyCheck.mandatory(this, "recordsManagementService", recordsManagementService); + PropertyCheck.mandatory(this, "recordsManagementAdminService", recordsManagementAdminService); + PropertyCheck.mandatory(this, "recordsManagementEventService", recordsManagementEventService); + for(AbstractCapability capability : capabilities) + { + capability.registerAction(this); + } + } + + /** + * @see org.alfresco.repo.action.CommonResourceAbstractBase#setBeanName(java.lang.String) + */ + @Override + public void setBeanName(String name) + { + this.name = name; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementAction#getName() + */ + public String getName() + { + return this.name; + } + + /* + * @see org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction#getLabel() + */ + public String getLabel() + { + String label = I18NUtil.getMessage(this.getTitleKey()); + + if (label == null) + { + // default to the name of the action with first letter capitalised + label = StringUtils.capitalize(this.name); + } + + return label; + } + + /* + * @see org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction#getDescription() + */ + public String getDescription() + { + String desc = I18NUtil.getMessage(this.getDescriptionKey()); + + if (desc == null) + { + // default to the name of the action with first letter capitalised + desc = StringUtils.capitalize(this.name); + } + + return desc; + } + + /** + * By default an action is not a disposition action + * + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementAction#isDispositionAction() + */ + public boolean isDispositionAction() + { + return false; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementAction#execute(org.alfresco.service.cmr.repository.NodeRef, java.util.Map) + */ + public RecordsManagementActionResult execute(NodeRef filePlanComponent, Map parameters) + { + isExecutableImpl(filePlanComponent, parameters, true); + + // Create the action + Action action = this.actionService.createAction(name); + action.setParameterValues(parameters); + + recordsManagementAuditService.auditRMAction(this, filePlanComponent, parameters); + + // Execute the action + this.actionService.executeAction(action, filePlanComponent); + + // Get the result + Object value = action.getParameterValue(ActionExecuterAbstractBase.PARAM_RESULT); + return new RecordsManagementActionResult(value); + } + + /** + * Function to pad a string with zero '0' characters to the required length + * + * @param s String to pad with leading zero '0' characters + * @param len Length to pad to + * + * @return padded string or the original if already at >=len characters + */ + protected String padString(String s, int len) + { + String result = s; + for (int i=0; i<(len - s.length()); i++) + { + result = "0" + result; + } + return result; + } + + /** + * By default there are no parameters. + * + * @see org.alfresco.repo.action.ParameterizedItemAbstractBase#addParameterDefinitions(java.util.List) + */ + @Override + protected void addParameterDefinitions(List paramList) + { + // No parameters + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction#getProtectedProperties() + */ + public Set getProtectedProperties() + { + return Collections.emptySet(); + } + + + /** + * @see org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction#getProtectedAspects() + */ + public Set getProtectedAspects() + { + return Collections.emptySet(); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction#isExecutable(org.alfresco.service.cmr.repository.NodeRef, java.util.Map) + */ + public boolean isExecutable(NodeRef filePlanComponent, Map parameters) + { + return isExecutableImpl(filePlanComponent, parameters, false); + } + + protected abstract boolean isExecutableImpl(NodeRef filePlanComponent, Map parameters, boolean throwException); + + /** + * By default, rmActions do not provide an implicit target nodeRef. + */ + public NodeRef getImplicitTargetNodeRef() + { + return null; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementService#updateNextDispositionAction(org.alfresco.service.cmr.repository.NodeRef) + */ + public void updateNextDispositionAction(NodeRef nodeRef) + { + // Get this disposition instructions for the node + DispositionSchedule di = dispositionService.getDispositionSchedule(nodeRef); + if (di != null) + { + // Get the current action node + NodeRef currentDispositionAction = null; + if (this.nodeService.hasAspect(nodeRef, ASPECT_DISPOSITION_LIFECYCLE) == true) + { + List assocs = this.nodeService.getChildAssocs(nodeRef, ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL); + if (assocs.size() > 0) + { + currentDispositionAction = assocs.get(0).getChildRef(); + } + } + + if (currentDispositionAction != null) + { + // Move it to the history association + this.nodeService.moveNode(currentDispositionAction, nodeRef, ASSOC_DISPOSITION_ACTION_HISTORY, ASSOC_DISPOSITION_ACTION_HISTORY); + } + + List dispositionActionDefinitions = di.getDispositionActionDefinitions(); + DispositionActionDefinition currentDispositionActionDefinition = null; + DispositionActionDefinition nextDispositionActionDefinition = null; + + if (currentDispositionAction == null) + { + if (dispositionActionDefinitions.isEmpty() == false) + { + // The next disposition action is the first action + nextDispositionActionDefinition = dispositionActionDefinitions.get(0); + } + } + else + { + // Get the current action + String currentADId = (String)this.nodeService.getProperty(currentDispositionAction, PROP_DISPOSITION_ACTION_ID); + currentDispositionActionDefinition = di.getDispositionActionDefinition(currentADId); + + // Get the next disposition action + int index = currentDispositionActionDefinition.getIndex(); + index++; + if (index < dispositionActionDefinitions.size()) + { + nextDispositionActionDefinition = dispositionActionDefinitions.get(index); + } + } + + if (nextDispositionActionDefinition != null) + { + if (this.nodeService.hasAspect(nodeRef, ASPECT_DISPOSITION_LIFECYCLE) == false) + { + // Add the disposition life cycle aspect + this.nodeService.addAspect(nodeRef, ASPECT_DISPOSITION_LIFECYCLE, null); + } + + // Create the properties + Map props = new HashMap(10); + + // Calculate the asOf date + Date asOfDate = null; + Period period = nextDispositionActionDefinition.getPeriod(); + if (period != null) + { + // Use NOW as the default context date + Date contextDate = new Date(); + + // Get the period properties value + QName periodProperty = nextDispositionActionDefinition.getPeriodProperty(); + if (periodProperty != null) + { + contextDate = (Date)this.nodeService.getProperty(nodeRef, periodProperty); + + if (contextDate == null) + { + // TODO For now we will use NOW to resolve MOB-1184 + //throw new AlfrescoRuntimeException("Date used to calculate disposition action asOf date is not set for property " + periodProperty.toString()); + contextDate = new Date(); + } + } + + // Calculate the as of date + asOfDate = period.getNextDate(contextDate); + } + + // Set the property values + props.put(PROP_DISPOSITION_ACTION_ID, nextDispositionActionDefinition.getId()); + props.put(PROP_DISPOSITION_ACTION, nextDispositionActionDefinition.getName()); + if (asOfDate != null) + { + props.put(PROP_DISPOSITION_AS_OF, asOfDate); + } + + // Create a new disposition action object + NodeRef dispositionActionNodeRef = this.nodeService.createNode( + nodeRef, + ASSOC_NEXT_DISPOSITION_ACTION, + ASSOC_NEXT_DISPOSITION_ACTION, + TYPE_DISPOSITION_ACTION, + props).getChildRef(); + + // Create the events + List events = nextDispositionActionDefinition.getEvents(); + for (RecordsManagementEvent event : events) + { + // For every event create an entry on the action + createEvent(event, dispositionActionNodeRef); + } + } + } + } + + /** + * Creates the given records management event for the given 'next action'. + * + * @param event The event to create + * @param nextActionNodeRef The next action node + * @return The created event NodeRef + */ + protected NodeRef createEvent(RecordsManagementEvent event, NodeRef nextActionNodeRef) + { + NodeRef eventNodeRef = null; + + Map eventProps = new HashMap(7); + eventProps.put(PROP_EVENT_EXECUTION_NAME, event.getName()); + // TODO display label + RecordsManagementEventType eventType = recordsManagementEventService.getEventType(event.getType()); + eventProps.put(PROP_EVENT_EXECUTION_AUTOMATIC, eventType.isAutomaticEvent()); + eventProps.put(PROP_EVENT_EXECUTION_COMPLETE, false); + + // Create the event execution object + this.nodeService.createNode(nextActionNodeRef, ASSOC_EVENT_EXECUTIONS, + ASSOC_EVENT_EXECUTIONS, TYPE_EVENT_EXECUTION, eventProps); + + return eventNodeRef; + } + + /** + * Calculates and updates the rma:dispositionEventsEligible + * property for the given next disposition action. + * + * @param nextAction The next disposition action + * @return The result of calculation + */ + protected boolean updateEventEligible(DispositionAction nextAction) + { + List events = nextAction.getEventCompletionDetails(); + + boolean eligible = false; + if (nextAction.getDispositionActionDefinition().eligibleOnFirstCompleteEvent() == false) + { + eligible = true; + for (EventCompletionDetails event : events) + { + if (event.isEventComplete() == false) + { + eligible = false; + break; + } + } + } + else + { + for (EventCompletionDetails event : events) + { + if (event.isEventComplete() == true) + { + eligible = true; + break; + } + } + } + + // Update the property with the eligible value + this.nodeService.setProperty(nextAction.getNodeRef(), PROP_DISPOSITION_EVENTS_ELIGIBLE, eligible); + + return eligible; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/RMDispositionActionExecuterAbstractBase.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/RMDispositionActionExecuterAbstractBase.java new file mode 100644 index 0000000000..72bd5a7861 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/RMDispositionActionExecuterAbstractBase.java @@ -0,0 +1,393 @@ +/* + * 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.module.org_alfresco_module_rm.action; + +import java.io.Serializable; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.springframework.extensions.surf.util.I18NUtil; + +/** + * @author Roy Wetherall + */ +public abstract class RMDispositionActionExecuterAbstractBase extends RMActionExecuterAbstractBase +{ + /** I18N */ + private static final String MSG_RECORD_NOT_DECLARED = "rm.action.record-not-declared"; + private static final String MSG_EXPECTED_RECORD_LEVEL = "rm.action.expected-record-level"; + private static final String MSG_NOT_ALL_RECORDS_DECLARED = "rm.action.not-all-records-declared"; + private static final String MSG_NOT_ELIGIBLE = "rm.action.not-eligible"; + private static final String MSG_NO_DISPOITION_INSTRUCTIONS = "rm.action.no-disposition-instructions"; + private static final String MSG_NO_DIS_LIFECYCLE_SET = "rm.action.no-disposition-lisfecycle-set"; + private static final String MSG_NEXT_DISP_NOT_SET = "rm.action.next-disp-not-set"; + private static final String MSG_NOT_NEXT_DISP = "rm.action.not-next-disp"; + private static final String MSG_NOT_RECORD_FOLDER = "rm.action.not-record-folder"; + + /** Indicates whether the eligibility of the record should be checked or not */ + // TODO add the capability to override this value using a property on the action + protected boolean checkEligibility = true; + + /** + * All children of this implementation are disposition actions. + * + * @see org.alfresco.module.org_alfresco_module_rm.action.RMActionExecuterAbstractBase#isDispositionAction() + */ + @Override + public boolean isDispositionAction() + { + return true; + } + + /** + * Indicates whether the disposition is marked complete + * + * @return boolean true if marked complete, false otherwise + */ + public boolean getSetDispositionActionComplete() + { + return true; + } + + /** + * @see org.alfresco.repo.action.executer.ActionExecuterAbstractBase#executeImpl(org.alfresco.service.cmr.action.Action, + * org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + protected void executeImpl(Action action, NodeRef actionedUponNodeRef) + { + // + NodeRef nextDispositionActionNodeRef = getNextDispostionAction(actionedUponNodeRef); + + // Check the validity of the action (is it the next action, are we dealing with the correct type of object for + // the disposition level? + DispositionSchedule di = checkDispositionActionExecutionValidity(actionedUponNodeRef, nextDispositionActionNodeRef, true); + + // Check the eligibility of the action + if (checkEligibility == false || this.dispositionService.isNextDispositionActionEligible(actionedUponNodeRef) == true) + { + if (di.isRecordLevelDisposition() == true) + { + // Check that we do indeed have a record + if (this.recordsManagementService.isRecord(actionedUponNodeRef) == true) + { + // Can only execute disposition action on record if declared + if (this.recordsManagementService.isRecordDeclared(actionedUponNodeRef) == true) + { + // Indicate that the disposition action is underway + this.nodeService.setProperty(nextDispositionActionNodeRef, PROP_DISPOSITION_ACTION_STARTED_AT, new Date()); + this.nodeService.setProperty(nextDispositionActionNodeRef, PROP_DISPOSITION_ACTION_STARTED_BY, AuthenticationUtil.getRunAsUser()); + + // Execute record level disposition + executeRecordLevelDisposition(action, actionedUponNodeRef); + + if (this.nodeService.exists(nextDispositionActionNodeRef) == true && + getSetDispositionActionComplete() == true) + { + this.nodeService.setProperty(nextDispositionActionNodeRef, PROP_DISPOSITION_ACTION_COMPLETED_AT, new Date()); + this.nodeService.setProperty(nextDispositionActionNodeRef, PROP_DISPOSITION_ACTION_COMPLETED_BY, AuthenticationUtil.getRunAsUser()); + } + } + else + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_RECORD_NOT_DECLARED, getName(), actionedUponNodeRef.toString())); + } + } + else + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_EXPECTED_RECORD_LEVEL, getName(), actionedUponNodeRef.toString())); + } + } + else + { + if (this.recordsManagementService.isRecordFolder(actionedUponNodeRef) == true) + { + if (this.recordsManagementService.isRecordFolderDeclared(actionedUponNodeRef) == true) + { + // Indicate that the disposition action is underway + this.nodeService.setProperty(nextDispositionActionNodeRef, PROP_DISPOSITION_ACTION_STARTED_AT, new Date()); + this.nodeService.setProperty(nextDispositionActionNodeRef, PROP_DISPOSITION_ACTION_STARTED_BY, AuthenticationUtil.getRunAsUser()); + + executeRecordFolderLevelDisposition(action, actionedUponNodeRef); + + // Indicate that the disposition action is compelte + if (this.nodeService.exists(nextDispositionActionNodeRef) == true && + getSetDispositionActionComplete() == true) + { + this.nodeService.setProperty(nextDispositionActionNodeRef, PROP_DISPOSITION_ACTION_COMPLETED_AT, new Date()); + this.nodeService.setProperty(nextDispositionActionNodeRef, PROP_DISPOSITION_ACTION_COMPLETED_BY, AuthenticationUtil.getRunAsUser()); + } + + } + else + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_NOT_ALL_RECORDS_DECLARED, getName(), actionedUponNodeRef.toString())); + } + } + else + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_EXPECTED_RECORD_LEVEL, getName(), actionedUponNodeRef.toString())); + } + + } + + if (this.nodeService.exists(actionedUponNodeRef) == true && getSetDispositionActionComplete() == true) + { + // Update the disposition schedule + updateNextDispositionAction(actionedUponNodeRef); + } + } + else + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_NOT_ELIGIBLE, getName(), actionedUponNodeRef.toString())); + } + } + + /** + * @see org.alfresco.repo.action.ParameterizedItemAbstractBase#addParameterDefinitions(java.util.List) + */ + @Override + protected void addParameterDefinitions(List paramList) + { + // TODO add the "checkEligibility" parameter + } + + /** + * @param action + * @param record + */ + protected abstract void executeRecordLevelDisposition(Action action, NodeRef record); + + /** + * @param action + * @param recordFolder + */ + protected abstract void executeRecordFolderLevelDisposition(Action action, NodeRef recordFolder); + + /** + * @param nodeRef + * @return + */ + protected DispositionSchedule checkDispositionActionExecutionValidity(NodeRef nodeRef, NodeRef nextDispositionActionNodeRef, boolean throwError) + { + // Check the node has associated disposition instructions + DispositionSchedule di = this.dispositionService.getDispositionSchedule(nodeRef); + if (di == null) + { + if (throwError) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_NO_DISPOITION_INSTRUCTIONS, getName(), nodeRef.toString())); + } + else + { + return null; + } + } + + // Check the node has the disposition schedule aspect applied + if (this.nodeService.hasAspect(nodeRef, ASPECT_DISPOSITION_LIFECYCLE) == false) + { + if (throwError) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_NO_DIS_LIFECYCLE_SET, getName(), nodeRef.toString())); + } + else + { + return null; + } + } + + // Check this the next disposition action + + NodeRef nextDispositionAction = nextDispositionActionNodeRef; + if (nextDispositionAction == null) + { + if (throwError) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_NEXT_DISP_NOT_SET, getName(), nodeRef.toString())); + } + else + { + return null; + } + } + String actionName = (String) this.nodeService.getProperty(nextDispositionAction, PROP_DISPOSITION_ACTION); + if (actionName == null || actionName.equals(getName()) == false) + { + if (throwError) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_NOT_NEXT_DISP, getName(), nodeRef.toString())); + } + else + { + return null; + } + } + + return di; + } + + /** + * Get the next disposition action node. Null if none present. + * + * @param nodeRef + * the disposable node reference + * @return NodeRef the next disposition action, null if none + */ + private NodeRef getNextDispostionAction(NodeRef nodeRef) + { + NodeRef result = null; + List assocs = this.nodeService.getChildAssocs(nodeRef, ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL); + if (assocs.size() != 0) + { + result = assocs.get(0).getChildRef(); + } + return result; + } + + @Override + public Set getProtectedProperties() + { + HashSet qnames = new HashSet(); + qnames.add(PROP_DISPOSITION_ACTION_STARTED_AT); + qnames.add(PROP_DISPOSITION_ACTION_STARTED_BY); + qnames.add(PROP_DISPOSITION_ACTION_COMPLETED_AT); + qnames.add(PROP_DISPOSITION_ACTION_COMPLETED_BY); + return qnames; + } + + @Override + protected boolean isExecutableImpl(NodeRef filePlanComponent, Map parameters, boolean throwException) + { + // Check the validity of the action (is it the next action, are we dealing with the correct type of object for + // the disposition level? + // + NodeRef nextDispositionActionNodeRef = getNextDispostionAction(filePlanComponent); + + DispositionSchedule di = checkDispositionActionExecutionValidity(filePlanComponent, nextDispositionActionNodeRef, throwException); + + if(di == null) + { + if (throwException) + { + throw new AlfrescoRuntimeException("Null disposition"); + } + else + { + return false; + } + } + + // Check the eligibility of the action + if (checkEligibility == false || this.dispositionService.isNextDispositionActionEligible(filePlanComponent) == true) + { + if (di.isRecordLevelDisposition() == true) + { + // Check that we do indeed have a record + if (this.recordsManagementService.isRecord(filePlanComponent) == true) + { + // Can only execute disposition action on record if declared + if (this.recordsManagementService.isRecordDeclared(filePlanComponent) == true) + { + return true; + } + else + { + if (throwException) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_RECORD_NOT_DECLARED, getName(), filePlanComponent.toString())); + } + else + { + return false; + } + } + } + else + { + if (throwException) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_EXPECTED_RECORD_LEVEL, getName(), filePlanComponent.toString())); + } + else + { + return false; + } + } + } + else + { + if (this.recordsManagementService.isRecordFolder(filePlanComponent) == true) + { + if (this.recordsManagementService.isRecordFolderDeclared(filePlanComponent) == true) + { + return true; + } + else + { + if (throwException) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_NOT_ALL_RECORDS_DECLARED, getName(), filePlanComponent.toString())); + } + else + { + return false; + } + } + } + else + { + if (throwException) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_NOT_RECORD_FOLDER, getName(), filePlanComponent.toString())); + } + else + { + return false; + } + } + + } + + } + else + { + if (throwException) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_NOT_ELIGIBLE, getName(), filePlanComponent.toString())); + } + else + { + return false; + } + } + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/RecordsManagementAction.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/RecordsManagementAction.java new file mode 100644 index 0000000000..1e64b22162 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/RecordsManagementAction.java @@ -0,0 +1,106 @@ +/* + * 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.module.org_alfresco_module_rm.action; + +import java.io.Serializable; +import java.util.Map; +import java.util.Set; + +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + + +/** + * Record Management Action + * + * @author Roy Wetherall + */ +public interface RecordsManagementAction +{ + /** + * Get the name of the action + * + * @return String action name + */ + public String getName(); + + /** + * Get the label of the action + * + * @return String action label + */ + public String getLabel(); + + /** + * Get the description of the action + * + * @return String action description + */ + public String getDescription(); + + /** + * Indicates whether this is a disposition action or not + * + * @return boolean true if a disposition action, false otherwise + */ + boolean isDispositionAction(); + + /** + * Execution of the action + * + * @param filePlanComponent file plan component the action is executed upon + * @param parameters action parameters + */ + public RecordsManagementActionResult execute(NodeRef filePlanComponent, Map parameters); + + + /** + * Can this action be executed? + * Does it meet all of its entry requirements - EXCEPT permission checks. + * + * @param filePlanComponent file plan component the action is executed upon + * @param parameters action parameters + * @return + */ + public boolean isExecutable(NodeRef filePlanComponent, Map parameters); + + + /** + * Get a set of properties that should only be updated via this or other action. + * These properties will be rejected by updates via the generic public services, such as the NodeService. + * + * @return the set of protected properties + */ + public Set getProtectedProperties(); + + /** + * Get a set of aspects that should be updated via this or other actions. + * The aspect can not be added via public services, such as the NodeService. + * @return + */ + public Set getProtectedAspects(); + + /** + * Some admin-related rmActions execute against a target nodeRef which is not provided + * by the calling code, but is instead an implementation detail of the action. + * + * @return the target nodeRef + */ + public NodeRef getImplicitTargetNodeRef(); +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/RecordsManagementActionResult.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/RecordsManagementActionResult.java new file mode 100644 index 0000000000..583beefe40 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/RecordsManagementActionResult.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2009-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.module.org_alfresco_module_rm.action; + +/** + * Records management action result. + * + * @author Roy Wetherall + */ +public class RecordsManagementActionResult +{ + /** Result value */ + private Object value; + + /** + * Constructor. + * + * @param value result value + */ + public RecordsManagementActionResult(Object value) + { + this.value = value; + } + + /** + * @return result value + */ + public Object getValue() + { + return this.value; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/RecordsManagementActionService.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/RecordsManagementActionService.java new file mode 100644 index 0000000000..6d943d7412 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/RecordsManagementActionService.java @@ -0,0 +1,115 @@ +/* + * 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.module.org_alfresco_module_rm.action; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +import org.alfresco.service.cmr.repository.NodeRef; + + +/** + * Records management action service interface + * + * @author Roy Wetherall + */ +public interface RecordsManagementActionService +{ + /** + * Get a list of the available records management actions + * + * @return List records management actions + */ + List getRecordsManagementActions(); + + /** + * Get a list of the available disposition actions. A disposition action is a records + * management action that can be used when defining disposition instructions. + * + * @return List disposition actions + */ + List getDispositionActions(); + + /** + * Gets the named records management action + * + * @param name The name of the RM action to retrieve + * @return The RecordsManagementAction or null if it doesn't exist + */ + RecordsManagementAction getRecordsManagementAction(String name); + + /** + * Gets the named disposition action + * + * @param name The name of the disposition action to retrieve + * @return The RecordsManagementAction or null if it doesn't exist + */ + RecordsManagementAction getDispositionAction(String name); + + /** + * Execute a records management action + * + * @param nodeRef node reference to a rm container, rm folder or record + * @param name action name + */ + RecordsManagementActionResult executeRecordsManagementAction(NodeRef nodeRef, String name); + + /** + * Execute a records management action against several nodes + * + * @param nodeRefs node references to rm containers, rm folders or records + * @param name action name + */ + Map executeRecordsManagementAction(List nodeRefs, String name); + + /** + * Execute a records management action + * + * @param nodeRef node reference to a rm container, rm folder or record + * @param name action name + * @param parameters action parameters + */ + RecordsManagementActionResult executeRecordsManagementAction(NodeRef nodeRef, String name, Map parameters); + + /** + * Execute a records management action against several nodes + * + * @param nodeRefs node references to rm containers, rm folders or records + * @param name action name + * @param parameters action parameters + */ + Map executeRecordsManagementAction(List nodeRefs, String name, Map parameters); + + /** + * Execute a records management action. The nodeRef against which the action is to be + * executed must be provided by the RecordsManagementAction implementation. + * + * @param name action name + * @param parameters action parameters + */ + RecordsManagementActionResult executeRecordsManagementAction(String name, Map parameters); + + /** + * Register records management action + * + * @param rmAction records management action + */ + void register(RecordsManagementAction rmAction); +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/RecordsManagementActionServiceImpl.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/RecordsManagementActionServiceImpl.java new file mode 100644 index 0000000000..ac6eab0aaf --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/RecordsManagementActionServiceImpl.java @@ -0,0 +1,293 @@ +/* + * 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.module.org_alfresco_module_rm.action; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementPoliciesUtil; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementPolicies.BeforeRMActionExecution; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementPolicies.OnRMActionExecution; +import org.alfresco.repo.policy.ClassPolicyDelegate; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.surf.util.I18NUtil; + +/** + * Records Management Action Service Implementation + * + * @author Roy Wetherall + */ +public class RecordsManagementActionServiceImpl implements RecordsManagementActionService +{ + /** I18N */ + private static final String MSG_NOT_DEFINED = "rm.action.not-defined"; + private static final String MSG_NO_IMPLICIT_NODEREF = "rm.action.no-implicit-noderef"; + + /** Logger */ + private static Log logger = LogFactory.getLog(RecordsManagementActionServiceImpl.class); + + /** Registered records management actions */ + private Map rmActions = new HashMap(6); + private Map dispositionActions = new HashMap(4); + + /** Policy component */ + PolicyComponent policyComponent; + + /** Node service */ + NodeService nodeService; + + /** Policy delegates */ + private ClassPolicyDelegate beforeRMActionExecutionDelegate; + private ClassPolicyDelegate onRMActionExecutionDelegate; + + /** + * Set the policy component + * + * @param policyComponent policy component + */ + public void setPolicyComponent(PolicyComponent policyComponent) + { + this.policyComponent = policyComponent; + } + + /** + * Set the node service + * + * @param nodeService node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Initialise RM action service + */ + public void init() + { + // Register the various policies + beforeRMActionExecutionDelegate = policyComponent.registerClassPolicy(BeforeRMActionExecution.class); + onRMActionExecutionDelegate = policyComponent.registerClassPolicy(OnRMActionExecution.class); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementActionService#register(org.alfresco.module.org_alfresco_module_rm.RecordsManagementAction) + */ + public void register(RecordsManagementAction rmAction) + { + if (this.rmActions.containsKey(rmAction.getName()) == false) + { + if (logger.isDebugEnabled()) + { + logger.debug("Registering rmAction " + rmAction); + } + this.rmActions.put(rmAction.getName(), rmAction); + + if (rmAction.isDispositionAction() == true) + { + this.dispositionActions.put(rmAction.getName(), rmAction); + } + } + } + + /** + * Invoke beforeRMActionExecution policy + * + * @param nodeRef node reference + * @param name action name + * @param parameters action parameters + */ + protected void invokeBeforeRMActionExecution(NodeRef nodeRef, String name, Map parameters) + { + // get qnames to invoke against + Set qnames = RecordsManagementPoliciesUtil.getTypeAndAspectQNames(nodeService, nodeRef); + // execute policy for node type and aspects + BeforeRMActionExecution policy = beforeRMActionExecutionDelegate.get(qnames); + policy.beforeRMActionExecution(nodeRef, name, parameters); + } + + /** + * Invoke onRMActionExecution policy + * + * @param nodeRef node reference + * @param name action name + * @param parameters action parameters + */ + protected void invokeOnRMActionExecution(NodeRef nodeRef, String name, Map parameters) + { + // get qnames to invoke against + Set qnames = RecordsManagementPoliciesUtil.getTypeAndAspectQNames(nodeService, nodeRef); + // execute policy for node type and aspects + OnRMActionExecution policy = onRMActionExecutionDelegate.get(qnames); + policy.onRMActionExecution(nodeRef, name, parameters); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementActionService#getRecordsManagementActions() + */ + public List getRecordsManagementActions() + { + List result = new ArrayList(this.rmActions.size()); + result.addAll(this.rmActions.values()); + return Collections.unmodifiableList(result); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService#getDispositionActions(org.alfresco.service.cmr.repository.NodeRef) + */ + public List getDispositionActions(NodeRef nodeRef) + { + String userName = AuthenticationUtil.getFullyAuthenticatedUser(); + List result = new ArrayList(this.rmActions.size()); + + for (RecordsManagementAction action : this.rmActions.values()) + { + // TODO check the permissions on the action ... + } + + return Collections.unmodifiableList(result); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementActionService#getDispositionActionDefinitions() + */ + public List getDispositionActions() + { + List result = new ArrayList(this.rmActions.size()); + result.addAll(this.dispositionActions.values()); + return Collections.unmodifiableList(result); + } + + /* + * @see org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService#getDispositionAction(java.lang.String) + */ + public RecordsManagementAction getDispositionAction(String name) + { + return this.dispositionActions.get(name); + } + + /* + * @see org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService#getRecordsManagementAction(java.lang.String) + */ + public RecordsManagementAction getRecordsManagementAction(String name) + { + return this.rmActions.get(name); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementActionService#executeRecordsManagementAction(org.alfresco.service.cmr.repository.NodeRef, java.lang.String) + */ + public RecordsManagementActionResult executeRecordsManagementAction(NodeRef nodeRef, String name) + { + return executeRecordsManagementAction(nodeRef, name, null); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementActionService#executeRecordsManagementAction(java.util.List, java.lang.String) + */ + public Map executeRecordsManagementAction(List nodeRefs, String name) + { + return executeRecordsManagementAction(nodeRefs, name, null); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementActionService#executeRecordsManagementAction(org.alfresco.service.cmr.repository.NodeRef, java.lang.String, java.util.Map) + */ + public RecordsManagementActionResult executeRecordsManagementAction(NodeRef nodeRef, String name, Map parameters) + { + if (logger.isDebugEnabled()) + { + logger.debug("Executing record management action on " + nodeRef); + logger.debug(" actionName = " + name); + logger.debug(" parameters = " + parameters); + } + + RecordsManagementAction rmAction = this.rmActions.get(name); + if (rmAction == null) + { + String msg = I18NUtil.getMessage(MSG_NOT_DEFINED, name); + if (logger.isWarnEnabled()) + { + logger.warn(msg); + } + throw new AlfrescoRuntimeException(msg); + } + + // Execute action + invokeBeforeRMActionExecution(nodeRef, name, parameters); + RecordsManagementActionResult result = rmAction.execute(nodeRef, parameters); + if (nodeService.exists(nodeRef) == true) + { + invokeOnRMActionExecution(nodeRef, name, parameters); + } + + return result; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementActionService#executeRecordsManagementAction(java.lang.String, java.util.Map) + */ + public RecordsManagementActionResult executeRecordsManagementAction(String name, Map parameters) + { + RecordsManagementAction rmAction = rmActions.get(name); + + NodeRef implicitTargetNode = rmAction.getImplicitTargetNodeRef(); + if (implicitTargetNode == null) + { + String msg = I18NUtil.getMessage(MSG_NO_IMPLICIT_NODEREF, name); + if (logger.isWarnEnabled()) + { + logger.warn(msg); + } + throw new AlfrescoRuntimeException(msg); + } + else + { + return this.executeRecordsManagementAction(implicitTargetNode, name, parameters); + } + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementActionService#executeRecordsManagementAction(java.util.List, java.lang.String, java.util.Map) + */ + public Map executeRecordsManagementAction(List nodeRefs, String name, Map parameters) + { + // Execute the action on each node in the list + Map results = new HashMap(nodeRefs.size()); + for (NodeRef nodeRef : nodeRefs) + { + RecordsManagementActionResult result = executeRecordsManagementAction(nodeRef, name, parameters); + results.put(nodeRef, result); + } + + return results; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/ScheduledDispositionJob.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/ScheduledDispositionJob.java new file mode 100644 index 0000000000..d4dc38e8bf --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/ScheduledDispositionJob.java @@ -0,0 +1,129 @@ +/* + * 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.module.org_alfresco_module_rm.action; + +import java.util.Calendar; +import java.util.List; + +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +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.ResultSet; +import org.alfresco.service.cmr.search.SearchService; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; + +/** + * Scheduled disposition job. + * + * Automatically cuts off eligible nodes. + * + * @author Roy Wetherall + */ +public class ScheduledDispositionJob implements Job +{ + /** Logger */ + private static Log logger = LogFactory.getLog(ScheduledDispositionJob.class); + + /** + * @see org.quartz.Job#execute(org.quartz.JobExecutionContext) + */ + public void execute(JobExecutionContext context) throws JobExecutionException + { + RecordsManagementActionService rmActionService + = (RecordsManagementActionService)context.getJobDetail().getJobDataMap().get("recordsManagementActionService"); + NodeService nodeService = (NodeService)context.getJobDetail().getJobDataMap().get("nodeService"); + + + // Calculate the date range used in the query + Calendar cal = Calendar.getInstance(); + String year = String.valueOf(cal.get(Calendar.YEAR)); + String month = String.valueOf(cal.get(Calendar.MONTH) + 1); + String dayOfMonth = String.valueOf(cal.get(Calendar.DAY_OF_MONTH)); + + //TODO These pad() calls are in RMActionExecuterAbstractBase. I've copied them + // here as I have no access to that class. + + final String currentDate = padString(year, 2) + "-" + padString(month, 2) + + "-" + padString(dayOfMonth, 2) + "T00:00:00.00Z"; + + if (logger.isDebugEnabled() == true) + { + StringBuilder msg = new StringBuilder(); + msg.append("Executing ") + .append(this.getClass().getSimpleName()) + .append(" with currentDate ") + .append(currentDate); + logger.debug(msg.toString()); + } + + //TODO Copied the 1970 start date from the old RM JavaScript impl. + String dateRange = "[\"1970-01-01T00:00:00.00Z\" TO \"" + currentDate + "\"]"; + + // Execute the query and process the results + String query = "+ASPECT:\"rma:record\" +ASPECT:\"rma:dispositionSchedule\" +@rma\\:dispositionAsOf:" + dateRange; + + SearchService search = (SearchService)context.getJobDetail().getJobDataMap().get("searchService"); + ResultSet results = search.query(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, SearchService.LANGUAGE_LUCENE, query); + + List resultNodes = results.getNodeRefs(); + results.close(); + + if (logger.isDebugEnabled() == true) + { + StringBuilder msg = new StringBuilder(); + msg.append("Found ") + .append(resultNodes.size()) + .append(" records eligible for disposition."); + logger.debug(msg.toString()); + } + + for (NodeRef node : resultNodes ) + { + String dispActionName = (String)nodeService.getProperty(node, RecordsManagementModel.PROP_DISPOSITION_ACTION_NAME); + + // Only automatically execute "cutoff" actions. + // destroy and transfer and anything else should be manual for now + if (dispActionName != null && dispActionName.equalsIgnoreCase("cutoff")) + { + rmActionService.executeRecordsManagementAction(node, dispActionName); + + if (logger.isDebugEnabled() == true) + { + logger.debug("Performing " + dispActionName + " dispoition action on disposable item " + node.toString()); + } + } + } + } + + //TODO This has been pasted out of RMActionExecuterAbstractBase. To be relocated. + private String padString(String s, int len) + { + String result = s; + for (int i=0; i<(len - s.length()); i++) + { + result = "0" + result; + } + return result; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/ApplyCustomTypeAction.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/ApplyCustomTypeAction.java new file mode 100644 index 0000000000..01c5b71dcf --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/ApplyCustomTypeAction.java @@ -0,0 +1,159 @@ +/* + * 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.module.org_alfresco_module_rm.action.impl; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.module.org_alfresco_module_rm.action.RMActionExecuterAbstractBase; +import org.alfresco.repo.action.ParameterDefinitionImpl; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.dictionary.AspectDefinition; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.surf.util.I18NUtil; + +/** + * This class applies the aspect specified in the spring bean property customTypeAspect. + * It is used to apply one of the 4 "custom type" aspects from the DOD 5015 model. + * + * @author Neil McErlean + */ +public class ApplyCustomTypeAction extends RMActionExecuterAbstractBase +{ + /** I18N */ + private static final String MSG_ACTIONED_UPON_NOT_RECORD = "rm.action.actioned-upon-not-record"; + private static final String MSG_CUSTOM_ASPECT_NOT_RECOGNISED = "rm.action.custom-aspect-not-recognised"; + + private static Log logger = LogFactory.getLog(ApplyCustomTypeAction.class); + private QName customTypeAspect; + private List parameterDefinitions; + + public void setCustomTypeAspect(String customTypeAspect) + { + this.customTypeAspect = QName.createQName(customTypeAspect, namespaceService); + } + + /** + * @see org.alfresco.repo.action.executer.ActionExecuterAbstractBase#executeImpl(org.alfresco.service.cmr.action.Action, org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + protected void executeImpl(Action action, NodeRef actionedUponNodeRef) + { + if (logger.isDebugEnabled()) + { + logger.debug("Executing action [" + action.getActionDefinitionName() + "] on " + actionedUponNodeRef); + } + + // Apply the appropriate aspect and set the properties. + Map aspectProps = getPropertyValues(action); + this.nodeService.addAspect(actionedUponNodeRef, customTypeAspect, aspectProps); + } + + /** + * This method extracts the properties from the custom type's aspect. + * @see #getCustomTypeAspect() + */ + @Override + protected final void addParameterDefinitions(List paramList) + { + AspectDefinition aspectDef = dictionaryService.getAspect(customTypeAspect); + for (PropertyDefinition propDef : aspectDef.getProperties().values()) + { + QName propName = propDef.getName(); + QName propType = propDef.getDataType().getName(); + paramList.add(new ParameterDefinitionImpl(propName.toPrefixString(), propType, propDef.isMandatory(), null)); + } + } + + /** + * This method converts a Map of String, Serializable to a Map of QName, Serializable. + * To do this, it assumes that each parameter name is a String representing a qname + * of the form prefix:localName. + */ + private Map getPropertyValues(Action action) + { + Map paramValues = action.getParameterValues(); + + Map result = new HashMap(paramValues.size()); + for (String paramName : paramValues.keySet()) + { + QName propQName = QName.createQName(paramName, this.namespaceService); + result.put(propQName, paramValues.get(paramName)); + } + + return result; + } + + @Override + public boolean isExecutableImpl(NodeRef filePlanComponent, Map parameters, boolean throwException) + { + + if (recordsManagementService.isRecord(filePlanComponent)) + { + return true; + } + else + { + if (throwException) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_ACTIONED_UPON_NOT_RECORD, this.getClass().getSimpleName(), filePlanComponent.toString())); + } + else + { + return false; + } + } + } + + @Override + protected synchronized List getParameterDefintions() + { + // We can take these parameter definitions from the properties defined in the dod model. + if (this.parameterDefinitions == null) + { + AspectDefinition aspectDefinition = dictionaryService.getAspect(customTypeAspect); + if (aspectDefinition == null) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_CUSTOM_ASPECT_NOT_RECOGNISED, customTypeAspect)); + } + + Map props = aspectDefinition.getProperties(); + + this.parameterDefinitions = new ArrayList(props.size()); + + for (QName qn : props.keySet()) + { + String paramName = qn.toPrefixString(namespaceService); + QName paramType = props.get(qn).getDataType().getName(); + boolean paramIsMandatory = props.get(qn).isMandatory(); + parameterDefinitions.add(new ParameterDefinitionImpl(paramName, paramType, paramIsMandatory, null)); + } + } + return parameterDefinitions; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/BroadcastDispositionActionDefinitionUpdateAction.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/BroadcastDispositionActionDefinitionUpdateAction.java new file mode 100644 index 0000000000..f0068e8066 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/BroadcastDispositionActionDefinitionUpdateAction.java @@ -0,0 +1,293 @@ +/* + * 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.module.org_alfresco_module_rm.action.impl; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.module.org_alfresco_module_rm.action.RMActionExecuterAbstractBase; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionAction; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule; +import org.alfresco.module.org_alfresco_module_rm.event.EventCompletionDetails; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.Period; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Action to implement the consequences of a change to the value of the DispositionActionDefinition + * properties. When these properties are changed on a disposition schedule, then any associated + * disposition actions may need to be updated as a consequence. + * + * @author Neil McErlean + */ +public class BroadcastDispositionActionDefinitionUpdateAction extends RMActionExecuterAbstractBase +{ + /** Logger */ + private static Log logger = LogFactory.getLog(BroadcastDispositionActionDefinitionUpdateAction.class); + + public static final String NAME = "broadcastDispositionActionDefinitionUpdate"; + public static final String CHANGED_PROPERTIES = "changedProperties"; + + /** + * @see org.alfresco.repo.action.executer.ActionExecuterAbstractBase#executeImpl(org.alfresco.service.cmr.action.Action, + * org.alfresco.service.cmr.repository.NodeRef) + */ + @SuppressWarnings("unchecked") + @Override + protected void executeImpl(Action action, NodeRef actionedUponNodeRef) + { + if (RecordsManagementModel.TYPE_DISPOSITION_ACTION_DEFINITION.equals(nodeService.getType(actionedUponNodeRef)) == false) + { + return; + } + + List changedProps = (List)action.getParameterValue(CHANGED_PROPERTIES); + + // Navigate up the containment hierarchy to get the record category grandparent and schedule. + NodeRef dispositionScheduleNode = nodeService.getPrimaryParent(actionedUponNodeRef).getParentRef(); + NodeRef rmContainer = nodeService.getPrimaryParent(dispositionScheduleNode).getParentRef(); + DispositionSchedule dispositionSchedule = dispositionService.getAssociatedDispositionSchedule(rmContainer); + + List disposableItems = dispositionService.getDisposableItems(dispositionSchedule); + for (NodeRef disposableItem : disposableItems) + { + updateDisposableItem(dispositionSchedule, disposableItem, actionedUponNodeRef, changedProps); + } + } + + /** + * + * @param ds + * @param disposableItem + * @param dispositionActionDefinition + * @param changedProps + */ + private void updateDisposableItem(DispositionSchedule ds, NodeRef disposableItem, NodeRef dispositionActionDefinition, List changedProps) + { + // We need to check that this folder is under the management of the disposition schedule that + // has been updated + DispositionSchedule itemDs = dispositionService.getDispositionSchedule(disposableItem); + if (itemDs != null && + itemDs.getNodeRef().equals(ds.getNodeRef()) == true) + { + if (this.nodeService.hasAspect(disposableItem, ASPECT_DISPOSITION_LIFECYCLE)) + { + // disposition lifecycle already exists for node so process changes + processActionDefinitionChanges(dispositionActionDefinition, changedProps, disposableItem); + } + else + { + // disposition lifecycle does not exist on the node so setup disposition + updateNextDispositionAction(disposableItem); + } + } + } + + /** + * Processes all the changes applied to the given disposition + * action definition node for the given record or folder node. + * + * @param dispositionActionDef The disposition action definition node + * @param changedProps The set of properties changed on the action definition + * @param recordOrFolder The record or folder the changes potentially need to be applied to + */ + private void processActionDefinitionChanges(NodeRef dispositionActionDef, List changedProps, NodeRef recordOrFolder) + { + // check that the step being edited is the current step for the folder, + // if not, the change has no effect on the current step so ignore + DispositionAction nextAction = dispositionService.getNextDispositionAction(recordOrFolder); + if (doesChangedStepAffectNextAction(dispositionActionDef, nextAction)) + { + // the change does effect the nextAction for this node + // so go ahead and determine what needs updating + if (changedProps.contains(PROP_DISPOSITION_PERIOD)) + { + persistPeriodChanges(dispositionActionDef, nextAction); + } + + if (changedProps.contains(PROP_DISPOSITION_EVENT) || changedProps.contains(PROP_DISPOSITION_EVENT_COMBINATION)) + { + persistEventChanges(dispositionActionDef, nextAction); + } + + if (changedProps.contains(PROP_DISPOSITION_ACTION_NAME)) + { + String action = (String)nodeService.getProperty(dispositionActionDef, PROP_DISPOSITION_ACTION_NAME); + nodeService.setProperty(nextAction.getNodeRef(), PROP_DISPOSITION_ACTION, action); + } + } + } + + /** + * Determines whether the disposition action definition (step) being + * updated has any effect on the given next action + * + * @param dispositionActionDef The disposition action definition node + * @param nextAction The next disposition action + * @return true if the step change affects the next action + */ + private boolean doesChangedStepAffectNextAction(NodeRef dispositionActionDef, + DispositionAction nextAction) + { + boolean affectsNextAction = false; + + if (dispositionActionDef != null && nextAction != null) + { + // check whether the id of the action definition node being changed + // is the same as the id of the next action + String nextActionId = nextAction.getId(); + if (dispositionActionDef.getId().equals(nextActionId)) + { + affectsNextAction = true; + } + } + + return affectsNextAction; + } + + /** + * Persists any changes made to the period on the given disposition action + * definition on the given next action. + * + * @param dispositionActionDef The disposition action definition node + * @param nextAction The next disposition action + */ + private void persistPeriodChanges(NodeRef dispositionActionDef, DispositionAction nextAction) + { + Date newAsOfDate = null; + Period dispositionPeriod = (Period)nodeService.getProperty(dispositionActionDef, PROP_DISPOSITION_PERIOD); + + if (dispositionPeriod != null) + { + // calculate the new as of date as we have been provided a new period + Date now = new Date(); + newAsOfDate = dispositionPeriod.getNextDate(now); + } + + if (logger.isDebugEnabled()) + { + logger.debug("Set disposition as of date for next action '" + nextAction.getName() + + "' (" + nextAction.getNodeRef() + ") to: " + newAsOfDate); + } + + this.nodeService.setProperty(nextAction.getNodeRef(), PROP_DISPOSITION_AS_OF, newAsOfDate); + } + + /** + * Persists any changes made to the events on the given disposition action + * definition on the given next action. + * + * @param dispositionActionDef The disposition action definition node + * @param nextAction The next disposition action + */ + @SuppressWarnings("unchecked") + private void persistEventChanges(NodeRef dispositionActionDef, DispositionAction nextAction) + { + // go through the current events on the next action and remove any that are not present any more + List stepEvents = (List)nodeService.getProperty(dispositionActionDef, PROP_DISPOSITION_EVENT); + List eventsList = nextAction.getEventCompletionDetails(); + List nextActionEvents = new ArrayList(eventsList.size()); + for (EventCompletionDetails event : eventsList) + { + // take note of the event names present on the next action + String eventName = event.getEventName(); + nextActionEvents.add(eventName); + + // if the event has been removed delete from next action + if (stepEvents != null && stepEvents.contains(event.getEventName()) == false) + { + // remove the child association representing the event + nodeService.removeChild(nextAction.getNodeRef(), event.getNodeRef()); + + if (logger.isDebugEnabled()) + logger.debug("Removed '" + eventName + "' from next action '" + nextAction.getName() + + "' (" + nextAction.getNodeRef() + ")"); + } + } + + // go through the disposition action definition step events and add any new ones + if (stepEvents != null) + { + for (String eventName : stepEvents) + { + if (!nextActionEvents.contains(eventName)) + { + createEvent(recordsManagementEventService.getEvent(eventName), nextAction.getNodeRef()); + + if (logger.isDebugEnabled()) + { + logger.debug("Added '" + eventName + "' to next action '" + nextAction.getName() + + "' (" + nextAction.getNodeRef() + ")"); + } + } + } + } + + // finally since events may have changed re-calculate the events eligible flag + boolean eligible = updateEventEligible(nextAction); + + if (logger.isDebugEnabled()) + { + logger.debug("Set events eligible flag to '" + eligible + "' for next action '" + nextAction.getName() + + "' (" + nextAction.getNodeRef() + ")"); + } + } + + + @Override + protected void addParameterDefinitions(List paramList) + { + // Intentionally empty + } + + @Override + public boolean isExecutableImpl(NodeRef filePlanComponent, Map parameters, boolean throwException) + { + return true; + } + + @Override + public Set getProtectedProperties() + { + HashSet qnames = new HashSet(); + qnames.add(PROP_DISPOSITION_AS_OF); + qnames.add(PROP_DISPOSITION_EVENT); + qnames.add(PROP_DISPOSITION_EVENT_COMBINATION); + qnames.add(PROP_DISPOSITION_EVENTS_ELIGIBLE); + return qnames; + } + + @Override + public Set getProtectedAspects() + { + return Collections.emptySet(); + } + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/CloseRecordFolderAction.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/CloseRecordFolderAction.java new file mode 100644 index 0000000000..6c59750de6 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/CloseRecordFolderAction.java @@ -0,0 +1,104 @@ +/* + * 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.module.org_alfresco_module_rm.action.impl; + +import java.io.Serializable; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.module.org_alfresco_module_rm.action.RMActionExecuterAbstractBase; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.surf.util.I18NUtil; + +/** + * Action to close the records folder + * + * @author Roy Wetherall + */ +public class CloseRecordFolderAction extends RMActionExecuterAbstractBase +{ + /** I18N */ + private static final String MSG_CLOSE_RECORD_FOLDER_NOT_FOLDER = "rm.action.close-record-folder-not-folder"; + + /** + * @see org.alfresco.repo.action.executer.ActionExecuterAbstractBase#executeImpl(org.alfresco.service.cmr.action.Action, org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + protected void executeImpl(Action action, NodeRef actionedUponNodeRef) + { + // TODO check that the user in question has the correct permissions to close a records folder + + if (this.recordsManagementService.isRecordFolder(actionedUponNodeRef) == true) + { + Boolean isClosed = (Boolean)this.nodeService.getProperty(actionedUponNodeRef, PROP_IS_CLOSED); + if (Boolean.FALSE.equals(isClosed) == true) + { + this.nodeService.setProperty(actionedUponNodeRef, PROP_IS_CLOSED, true); + } + } + else + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_CLOSE_RECORD_FOLDER_NOT_FOLDER, actionedUponNodeRef.toString())); + } + } + + /** + * @see org.alfresco.repo.action.ParameterizedItemAbstractBase#addParameterDefinitions(java.util.List) + */ + @Override + protected void addParameterDefinitions(List paramList) + { + // TODO Auto-generated method stub + + } + + @Override + public Set getProtectedProperties() + { + HashSet qnames = new HashSet(); + qnames.add(PROP_IS_CLOSED); + return qnames; + } + + @Override + protected boolean isExecutableImpl(NodeRef filePlanComponent, Map parameters, boolean throwException) + { + if (this.recordsManagementService.isRecordFolder(filePlanComponent)) + { + return true; + } + else + { + if (throwException) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_CLOSE_RECORD_FOLDER_NOT_FOLDER, filePlanComponent.toString())); + } + else + { + return false; + } + } + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/CompleteEventAction.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/CompleteEventAction.java new file mode 100644 index 0000000000..50d674030d --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/CompleteEventAction.java @@ -0,0 +1,189 @@ +/* + * 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.module.org_alfresco_module_rm.action.impl; + +import java.io.Serializable; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.module.org_alfresco_module_rm.action.RMActionExecuterAbstractBase; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionAction; +import org.alfresco.module.org_alfresco_module_rm.event.EventCompletionDetails; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.surf.util.I18NUtil; + +/** + * Complete event action + * + * @author Roy Wetherall + */ +public class CompleteEventAction extends RMActionExecuterAbstractBase +{ + /** I18N */ + private static final String MSG_EVENT_NO_DISP_LC = "rm.action.event-no-disp-lc"; + + public static final String PARAM_EVENT_NAME = "eventName"; + public static final String PARAM_EVENT_COMPLETED_BY = "eventCompletedBy"; + public static final String PARAM_EVENT_COMPLETED_AT = "eventCompletedAt"; + + /** + * @see org.alfresco.repo.action.executer.ActionExecuterAbstractBase#executeImpl(org.alfresco.service.cmr.action.Action, org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + protected void executeImpl(Action action, NodeRef actionedUponNodeRef) + { + String eventName = (String)action.getParameterValue(PARAM_EVENT_NAME); + String eventCompletedBy = (String)action.getParameterValue(PARAM_EVENT_COMPLETED_BY); + Date eventCompletedAt = (Date)action.getParameterValue(PARAM_EVENT_COMPLETED_AT); + + if (this.nodeService.hasAspect(actionedUponNodeRef, ASPECT_DISPOSITION_LIFECYCLE) == true) + { + // Get the next disposition action + DispositionAction da = this.dispositionService.getNextDispositionAction(actionedUponNodeRef); + if (da != null) + { + // Get the disposition event + EventCompletionDetails event = getEvent(da, eventName); + if (event != null) + { + // Update the event so that it is complete + NodeRef eventNodeRef = event.getNodeRef(); + Map props = this.nodeService.getProperties(eventNodeRef); + props.put(PROP_EVENT_EXECUTION_COMPLETE, true); + props.put(PROP_EVENT_EXECUTION_COMPLETED_AT, eventCompletedAt); + props.put(PROP_EVENT_EXECUTION_COMPLETED_BY, eventCompletedBy); + this.nodeService.setProperties(eventNodeRef, props); + + // Check to see if the events eligible property needs to be updated + updateEventEligible(da); + + } + else + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_EVENT_NO_DISP_LC, eventName)); + } + } + } + } + + /** + * Get the event from the dispostion action + * + * @param da + * @param eventName + * @return + */ + private EventCompletionDetails getEvent(DispositionAction da, String eventName) + { + EventCompletionDetails result = null; + List events = da.getEventCompletionDetails(); + for (EventCompletionDetails event : events) + { + if (eventName.equals(event.getEventName()) == true) + { + result = event; + break; + } + } + return result; + } + + /** + * @see org.alfresco.repo.action.ParameterizedItemAbstractBase#addParameterDefinitions(java.util.List) + */ + @Override + protected void addParameterDefinitions(List paramList) + { + // TODO add parameter definitions .... + // eventId, executeBy, executedAt + + } + + @Override + public Set getProtectedProperties() + { + HashSet qnames = new HashSet(); + qnames.add(PROP_EVENT_EXECUTION_COMPLETE); + qnames.add(PROP_EVENT_EXECUTION_COMPLETED_AT); + qnames.add(PROP_EVENT_EXECUTION_COMPLETED_BY); + return qnames; + } + + + + @Override + public Set getProtectedAspects() + { + HashSet qnames = new HashSet(); + qnames.add(ASPECT_DISPOSITION_LIFECYCLE); + return qnames; + } + + @Override + protected boolean isExecutableImpl(NodeRef filePlanComponent, Map parameters, boolean throwException) + { + String eventName = null; + if(parameters != null) + { + eventName = (String) parameters.get(PARAM_EVENT_NAME); + } + + if (this.nodeService.hasAspect(filePlanComponent, ASPECT_DISPOSITION_LIFECYCLE)) + { + // Get the next disposition action + DispositionAction da = this.dispositionService.getNextDispositionAction(filePlanComponent); + if (da != null) + { + // Get the disposition event + if(parameters != null) + { + EventCompletionDetails event = getEvent(da, eventName); + if (event != null) + { + return true; + } + else + { + if (throwException) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_EVENT_NO_DISP_LC, eventName)); + } + else + { + return false; + } + } + } + else + { + return true; + } + } + } + return false; + } + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/CreateDispositionScheduleAction.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/CreateDispositionScheduleAction.java new file mode 100644 index 0000000000..f03a60cce7 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/CreateDispositionScheduleAction.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.module.org_alfresco_module_rm.action.impl; + +import java.io.Serializable; +import java.util.Map; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.module.org_alfresco_module_rm.action.RMActionExecuterAbstractBase; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.repository.NodeRef; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Create disposition schedule action + * + * @author Roy Wetherall + */ +public class CreateDispositionScheduleAction extends RMActionExecuterAbstractBase +{ + /** Logger */ + @SuppressWarnings("unused") + private static Log logger = LogFactory.getLog(CreateDispositionScheduleAction.class); + + /** + * @see org.alfresco.repo.action.executer.ActionExecuterAbstractBase#executeImpl(org.alfresco.service.cmr.action.Action, org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + protected void executeImpl(Action action, NodeRef actionedUponNodeRef) + { + // Create the disposition schedule + dispositionService.createDispositionSchedule(actionedUponNodeRef, null); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.action.RMActionExecuterAbstractBase#isExecutableImpl(org.alfresco.service.cmr.repository.NodeRef, java.util.Map, boolean) + */ + @Override + public boolean isExecutableImpl(NodeRef filePlanComponent, Map parameters, boolean throwException) + { + boolean result = true; + if (recordsManagementService.isRecordCategory(filePlanComponent) == false) + { + if (throwException == true) + { + throw new AlfrescoRuntimeException("The disposition schedule could not be created, because the actioned upon node was not a record category."); + } + else + { + result = false; + } + } + + return result; + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/CutOffAction.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/CutOffAction.java new file mode 100644 index 0000000000..299b48614c --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/CutOffAction.java @@ -0,0 +1,139 @@ +/* + * 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.module.org_alfresco_module_rm.action.impl; + +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.module.org_alfresco_module_rm.action.RMDispositionActionExecuterAbstractBase; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.surf.util.I18NUtil; + +/** + * Cut off disposition action + * + * @author Roy Wetherall + */ +public class CutOffAction extends RMDispositionActionExecuterAbstractBase +{ + private static final String MSG_ERR = "rm.action.close-record-folder-not-folder"; + + /** + * @see org.alfresco.module.org_alfresco_module_rm.action.RMDispositionActionExecuterAbstractBase#executeRecordFolderLevelDisposition(org.alfresco.service.cmr.action.Action, org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + protected void executeRecordFolderLevelDisposition(Action action, NodeRef recordFolder) + { + // Close folder + Boolean isClosed = (Boolean)this.nodeService.getProperty(recordFolder, PROP_IS_CLOSED); + if (Boolean.FALSE.equals(isClosed) == true) + { + this.nodeService.setProperty(recordFolder, PROP_IS_CLOSED, true); + } + + // Mark the folder as cut off + doCutOff(recordFolder); + + // Mark all the declared children of the folder as cut off + List records = this.recordsManagementService.getRecords(recordFolder); + for (NodeRef record : records) + { + doCutOff(record); + } + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.action.RMDispositionActionExecuterAbstractBase#executeRecordLevelDisposition(org.alfresco.service.cmr.action.Action, org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + protected void executeRecordLevelDisposition(Action action, NodeRef record) + { + // Mark the record as cut off + doCutOff(record); + } + + /** + * Marks the record or record folder as cut off, calculating the cut off date. + * + * @param nodeRef node reference + */ + private void doCutOff(NodeRef nodeRef) + { + if (this.nodeService.hasAspect(nodeRef, ASPECT_CUT_OFF) == false) + { + // Apply the cut off aspect and set cut off date + Map cutOffProps = new HashMap(1); + cutOffProps.put(PROP_CUT_OFF_DATE, new Date()); + this.nodeService.addAspect(nodeRef, ASPECT_CUT_OFF, cutOffProps); + } + } + + @Override + public Set getProtectedProperties() + { + HashSet qnames = new HashSet(); + qnames.add(PROP_CUT_OFF_DATE); + return qnames; + } + + @Override + public Set getProtectedAspects() + { + HashSet qnames = new HashSet(); + qnames.add(ASPECT_CUT_OFF); + return qnames; + } + + @Override + protected boolean isExecutableImpl(NodeRef filePlanComponent, Map parameters, boolean throwException) + { + if(!super.isExecutableImpl(filePlanComponent, parameters, throwException)) + { + return false; + } + + // duplicates code from close .. it should get the closed action somehow? + if (this.recordsManagementService.isRecordFolder(filePlanComponent) + || this.recordsManagementService.isRecord(filePlanComponent)) + { + return true; + } + else + { + if (throwException) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_ERR, filePlanComponent.toString())); + } + else + { + return false; + } + } + } + + + } \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/DeclareRecordAction.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/DeclareRecordAction.java new file mode 100644 index 0000000000..d37078ad1c --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/DeclareRecordAction.java @@ -0,0 +1,236 @@ +/* + * 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.module.org_alfresco_module_rm.action.impl; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.module.org_alfresco_module_rm.action.RMActionExecuterAbstractBase; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.dictionary.AspectDefinition; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.dictionary.TypeDefinition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.OwnableService; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.surf.util.I18NUtil; + +/** + * Declare record action + * + * @author Roy Wetherall + */ +public class DeclareRecordAction extends RMActionExecuterAbstractBase +{ + /** I18N */ + private static final String MSG_UNDECLARED_ONLY_RECORDS = "rm.action.undeclared-only-records"; + private static final String MSG_NO_DECLARE_MAND_PROP = "rm.action.no-declare-mand-prop"; + + /** Logger */ + private static Log logger = LogFactory.getLog(DeclareRecordAction.class); + + /** + * @see org.alfresco.repo.action.executer.ActionExecuterAbstractBase#executeImpl(org.alfresco.service.cmr.action.Action, + * org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + protected void executeImpl(Action action, NodeRef actionedUponNodeRef) + { + if (recordsManagementService.isRecord(actionedUponNodeRef) == true) + { + if (recordsManagementService.isRecordDeclared(actionedUponNodeRef) == false) + { + List missingProperties = new ArrayList(5); + // Aspect not already defined - check mandatory properties then add + if (mandatoryPropertiesSet(actionedUponNodeRef, missingProperties) == true) + { + // Add the declared aspect + Map declaredProps = new HashMap(2); + declaredProps.put(PROP_DECLARED_AT, new Date()); + declaredProps.put(PROP_DECLARED_BY, AuthenticationUtil.getRunAsUser()); + this.nodeService.addAspect(actionedUponNodeRef, ASPECT_DECLARED_RECORD, declaredProps); + + // remove all owner related rights + this.ownableService.setOwner(actionedUponNodeRef, OwnableService.NO_OWNER); + } + else + { + throw new AlfrescoRuntimeException(buildMissingPropertiesErrorString(missingProperties)); + } + } + } + else + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_UNDECLARED_ONLY_RECORDS, actionedUponNodeRef.toString())); + } + } + + private String buildMissingPropertiesErrorString(List missingProperties) + { + StringBuilder builder = new StringBuilder(255); + builder.append(I18NUtil.getMessage(MSG_NO_DECLARE_MAND_PROP)); + builder.append(" "); + for (String missingProperty : missingProperties) + { + builder.append(missingProperty) + .append(", "); + } + return builder.toString(); + } + + /** + * Helper method to check whether all the mandatory properties of the node have been set + * + * @param nodeRef + * node reference + * @return boolean true if all mandatory properties are set, false otherwise + */ + private boolean mandatoryPropertiesSet(NodeRef nodeRef, List missingProperties) + { + boolean result = true; + + Map nodeRefProps = this.nodeService.getProperties(nodeRef); + + QName nodeRefType = this.nodeService.getType(nodeRef); + + TypeDefinition typeDef = this.dictionaryService.getType(nodeRefType); + for (PropertyDefinition propDef : typeDef.getProperties().values()) + { + if (propDef.isMandatory() == true) + { + if (nodeRefProps.get(propDef.getName()) == null) + { + logMissingProperty(propDef, missingProperties); + + result = false; + break; + } + } + } + + if (result != false) + { + Set aspects = this.nodeService.getAspects(nodeRef); + for (QName aspect : aspects) + { + AspectDefinition aspectDef = this.dictionaryService.getAspect(aspect); + for (PropertyDefinition propDef : aspectDef.getProperties().values()) + { + if (propDef.isMandatory() == true) + { + if (nodeRefProps.get(propDef.getName()) == null) + { + logMissingProperty(propDef, missingProperties); + + result = false; + break; + } + } + } + } + } + + return result; + } + + /** + * Log information about missing properties. + * + * @param propDef property definition + * @param missingProperties missing properties + */ + private void logMissingProperty(PropertyDefinition propDef, List missingProperties) + { + if (logger.isWarnEnabled()) + { + StringBuilder msg = new StringBuilder(); + msg.append("Mandatory property missing: ").append(propDef.getName()); + logger.warn(msg.toString()); + } + missingProperties.add(propDef.getName().toString()); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.action.RMActionExecuterAbstractBase#getProtectedAspects() + */ + @Override + public Set getProtectedAspects() + { + HashSet qnames = new HashSet(); + qnames.add(ASPECT_DECLARED_RECORD); + return qnames; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.action.RMActionExecuterAbstractBase#isExecutableImpl(org.alfresco.service.cmr.repository.NodeRef, java.util.Map, boolean) + */ + @Override + protected boolean isExecutableImpl(NodeRef filePlanComponent, Map parameters, boolean throwException) + { + if (recordsManagementService.isRecord(filePlanComponent) == true) + { + if (recordsManagementService.isRecordDeclared(filePlanComponent) == false) + { + // Aspect not already defined - check mandatory properties then add + List missingProperties = new ArrayList(10); + if (mandatoryPropertiesSet(filePlanComponent, missingProperties) == true) + { + return true; + } + else + { + if (throwException) + { + throw new AlfrescoRuntimeException(buildMissingPropertiesErrorString(missingProperties)); + } + else + { + return false; + } + } + } + else + { + return false; + } + } + else + { + if (throwException) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_UNDECLARED_ONLY_RECORDS, filePlanComponent.toString())); + } + else + { + return false; + } + } + } + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/DestroyAction.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/DestroyAction.java new file mode 100644 index 0000000000..6d4e5b8a82 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/DestroyAction.java @@ -0,0 +1,233 @@ +/* + * 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.module.org_alfresco_module_rm.action.impl; + +import java.io.Serializable; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.model.RenditionModel; +import org.alfresco.module.org_alfresco_module_rm.action.RMDispositionActionExecuterAbstractBase; +import org.alfresco.repo.content.ContentServicePolicies; +import org.alfresco.repo.content.cleanup.EagerContentStoreCleaner; +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.extensions.surf.util.I18NUtil; + +/** + * Destroy action. + * + * @author Roy Wetherall + */ +public class DestroyAction extends RMDispositionActionExecuterAbstractBase + implements ContentServicePolicies.OnContentUpdatePolicy, + InitializingBean +{ + /** I18N */ + private static final String MSG_GHOSTED_PROP_UPDATE = "rm.action.ghosted-prop-update"; + + /** Policy component */ + private PolicyComponent policyComponent; + + /** Eager content store cleaner */ + private EagerContentStoreCleaner eagerContentStoreCleaner; + + /** Indicates if ghosting is enabled or not */ + private boolean ghostingEnabled = true; + + /** + * @param policyComponent policy component + */ + public void setPolicyComponent(PolicyComponent policyComponent) + { + this.policyComponent = policyComponent; + } + + /** + * @param eagerContentStoreCleaner eager content store cleaner + */ + public void setEagerContentStoreCleaner(EagerContentStoreCleaner eagerContentStoreCleaner) + { + this.eagerContentStoreCleaner = eagerContentStoreCleaner; + } + + /** + * @param ghostingEnabled true if ghosting is enabled, false otherwise + */ + public void setGhostingEnabled(boolean ghostingEnabled) + { + this.ghostingEnabled = ghostingEnabled; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.action.RMDispositionActionExecuterAbstractBase#executeRecordFolderLevelDisposition(org.alfresco.service.cmr.action.Action, org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + protected void executeRecordFolderLevelDisposition(Action action, NodeRef recordFolder) + { + List records = this.recordsManagementService.getRecords(recordFolder); + for (NodeRef record : records) + { + executeRecordLevelDisposition(action, record); + } + + if (ghostingEnabled == true) + { + nodeService.addAspect(recordFolder, ASPECT_GHOSTED, Collections. emptyMap()); + } + else + { + nodeService.deleteNode(recordFolder); + } + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.action.RMDispositionActionExecuterAbstractBase#executeRecordLevelDisposition(org.alfresco.service.cmr.action.Action, org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + protected void executeRecordLevelDisposition(Action action, NodeRef record) + { + doDestroy(record); + } + + /** + * Do the content destroy + * + * @param nodeRef + */ + private void doDestroy(NodeRef nodeRef) + { + // Clear the content + clearAllContent(nodeRef); + + // Clear thumbnail content + clearThumbnails(nodeRef); + + if (ghostingEnabled == true) + { + // Add the ghosted aspect + nodeService.addAspect(nodeRef, ASPECT_GHOSTED, null); + } + else + { + // If ghosting is not enabled, delete the node + nodeService.deleteNode(nodeRef); + } + } + + /** + * Clear all the content properties + * + * @param nodeRef + */ + private void clearAllContent(NodeRef nodeRef) + { + Set props = this.nodeService.getProperties(nodeRef).keySet(); + props.retainAll(this.dictionaryService.getAllProperties(DataTypeDefinition.CONTENT)); + for (QName prop : props) + { + // Clear the content + clearContent(nodeRef, prop); + + // Remove the property + this.nodeService.removeProperty(nodeRef, prop); + } + } + + /** + * Clear all the thumbnail information + * + * @param nodeRef + */ + @SuppressWarnings("deprecation") + private void clearThumbnails(NodeRef nodeRef) + { + // Remove the renditioned aspect (and its properties and associations) if it is present. + // + // From Alfresco 3.3 it is the rn:renditioned aspect which defines the + // child-association being considered in this method. + // Note also that the cm:thumbnailed aspect extends the rn:renditioned aspect. + // + // We want to remove the rn:renditioned aspect, but due to the possibility + // that there is Alfresco 3.2-era data with the cm:thumbnailed aspect + // applied, we must consider removing it too. + if (nodeService.hasAspect(nodeRef, RenditionModel.ASPECT_RENDITIONED) || + nodeService.hasAspect(nodeRef, ContentModel.ASPECT_THUMBNAILED)) + { + // Add the ghosted aspect to all the renditioned children, so that they will not be archived when the + // renditioned aspect is removed + Set childAssocTypes = dictionaryService.getAspect(RenditionModel.ASPECT_RENDITIONED).getChildAssociations().keySet(); + for (ChildAssociationRef child : nodeService.getChildAssocs(nodeRef)) + { + if (childAssocTypes.contains(child.getTypeQName())) + { + // Clear the content and delete the rendition + clearAllContent(child.getChildRef()); + nodeService.deleteNode(child.getChildRef()); + } + } + } + } + + /** + * Clear a content property + * + * @param nodeRef + * @param contentProperty + */ + private void clearContent(NodeRef nodeRef, QName contentProperty) + { + // Ensure the content is cleaned at the end of the transaction + ContentData contentData = (ContentData)nodeService.getProperty(nodeRef, contentProperty); + if (contentData != null && contentData.getContentUrl() != null) + { + eagerContentStoreCleaner.registerOrphanedContentUrl(contentData.getContentUrl(), true); + } + } + + /** + * @see org.alfresco.repo.content.ContentServicePolicies.OnContentUpdatePolicy#onContentUpdate(org.alfresco.service.cmr.repository.NodeRef, boolean) + */ + public void onContentUpdate(NodeRef nodeRef, boolean newContent) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_GHOSTED_PROP_UPDATE)); + } + + /** + * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() + */ + public void afterPropertiesSet() throws Exception + { + // Register interest in the onContentUpdate policy + policyComponent.bindClassBehaviour( + ContentServicePolicies.OnContentUpdatePolicy.QNAME, + ASPECT_GHOSTED, + new JavaBehaviour(this, "onContentUpdate")); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/EditDispositionActionAsOfDateAction.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/EditDispositionActionAsOfDateAction.java new file mode 100644 index 0000000000..f1a8e45609 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/EditDispositionActionAsOfDateAction.java @@ -0,0 +1,122 @@ +/* + * 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.module.org_alfresco_module_rm.action.impl; + +import java.io.Serializable; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.module.org_alfresco_module_rm.action.RMActionExecuterAbstractBase; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionAction; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.surf.util.I18NUtil; + +/** + * Edit review as of date action. + * + * @author Roy Wetherall + */ +public class EditDispositionActionAsOfDateAction extends RMActionExecuterAbstractBase +{ + /** I18N */ + private static final String MSG_VALID_DATE_DISP_ASOF = "rm.action.valid-date-disp-asof"; + private static final String MSG_DISP_ASOF_LIFECYCLE_APPLIED = "rm.action.disp-asof-lifecycle-applied"; + + /** Logger */ + @SuppressWarnings("unused") + private static Log logger = LogFactory.getLog(EditDispositionActionAsOfDateAction.class); + + /** Action parameters */ + public static final String PARAM_AS_OF_DATE = "asOfDate"; + + /** + * @see org.alfresco.repo.action.executer.ActionExecuterAbstractBase#executeImpl(org.alfresco.service.cmr.action.Action, + * org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + protected void executeImpl(Action action, NodeRef actionedUponNodeRef) + { + if (this.nodeService.hasAspect(actionedUponNodeRef, ASPECT_DISPOSITION_LIFECYCLE) == true) + { + // Get the action parameter + Date asOfDate = (Date)action.getParameterValue(PARAM_AS_OF_DATE); + if (asOfDate == null) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_VALID_DATE_DISP_ASOF)); + } + + // Set the dispostion action as of date + DispositionAction da = dispositionService.getNextDispositionAction(actionedUponNodeRef); + if (da != null) + { + nodeService.setProperty(da.getNodeRef(), PROP_DISPOSITION_AS_OF, asOfDate); + } + } + } + + /** + * @see org.alfresco.repo.action.ParameterizedItemAbstractBase#addParameterDefinitions(java.util.List) + */ + @Override + protected void addParameterDefinitions(List paramList) + { + // Intentionally empty + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.action.RMActionExecuterAbstractBase#getProtectedProperties() + */ + @Override + public Set getProtectedProperties() + { + HashSet qnames = new HashSet(); + qnames.add(PROP_DISPOSITION_AS_OF); + return qnames; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.action.RMActionExecuterAbstractBase#isExecutableImpl(org.alfresco.service.cmr.repository.NodeRef, java.util.Map, boolean) + */ + @Override + protected boolean isExecutableImpl(NodeRef filePlanComponent, Map parameters, boolean throwException) + { + boolean result = false; + if (this.nodeService.hasAspect(filePlanComponent, ASPECT_DISPOSITION_LIFECYCLE) == true) + { + result = true; + } + else + { + if (throwException == true) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_DISP_ASOF_LIFECYCLE_APPLIED)); + } + } + return result; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/EditHoldReasonAction.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/EditHoldReasonAction.java new file mode 100644 index 0000000000..f840f8e42f --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/EditHoldReasonAction.java @@ -0,0 +1,119 @@ +/* + * 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.module.org_alfresco_module_rm.action.impl; + +import java.io.Serializable; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.module.org_alfresco_module_rm.action.RMActionExecuterAbstractBase; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.surf.util.I18NUtil; + +/** + * Edit freeze reason Action + * + * @author Roy Wetherall + */ +public class EditHoldReasonAction extends RMActionExecuterAbstractBase +{ + private static final String MSG_HOLD_EDIT_REASON_NONE = "rm.action.hold-edit-reason-none"; + private static final String MSG_HOLD_EDIT_TYPE = "rm.action.hold-edit-type"; + + /** Parameter names */ + public static final String PARAM_REASON = "reason"; + + /** + * @see org.alfresco.repo.action.executer.ActionExecuterAbstractBase#executeImpl(org.alfresco.service.cmr.action.Action, org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + protected void executeImpl(Action action, NodeRef actionedUponNodeRef) + { + QName nodeType = this.nodeService.getType(actionedUponNodeRef); + if (this.dictionaryService.isSubClass(nodeType, TYPE_HOLD) == true) + { + // Get the property values + String reason = (String)action.getParameterValue(PARAM_REASON); + if (reason == null || reason.length() == 0) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_HOLD_EDIT_REASON_NONE)); + } + + // Set the hold reason + nodeService.setProperty(actionedUponNodeRef, PROP_HOLD_REASON, reason); + + } + else + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_HOLD_EDIT_TYPE, TYPE_HOLD.toString(), actionedUponNodeRef.toString())); + } + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.action.RMActionExecuterAbstractBase#getProtectedAspects() + */ + @Override + public Set getProtectedAspects() + { + HashSet qnames = new HashSet(); + qnames.add(ASPECT_FROZEN); + return qnames; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.action.RMActionExecuterAbstractBase#getProtectedProperties() + */ + @Override + public Set getProtectedProperties() + { + HashSet qnames = new HashSet(); + qnames.add(PROP_HOLD_REASON); + return qnames; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.action.RMActionExecuterAbstractBase#isExecutableImpl(org.alfresco.service.cmr.repository.NodeRef, java.util.Map, boolean) + */ + @Override + protected boolean isExecutableImpl(NodeRef filePlanComponent, Map parameters, boolean throwException) + { + QName nodeType = this.nodeService.getType(filePlanComponent); + if (this.dictionaryService.isSubClass(nodeType, TYPE_HOLD) == true) + { + return true; + } + else + { + if(throwException) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_HOLD_EDIT_TYPE, TYPE_HOLD.toString(), filePlanComponent.toString())); + } + else + { + return false; + } + } + } + + +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/EditReviewAsOfDateAction.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/EditReviewAsOfDateAction.java new file mode 100644 index 0000000000..114ba6eb79 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/EditReviewAsOfDateAction.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.module.org_alfresco_module_rm.action.impl; + +import java.io.Serializable; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.module.org_alfresco_module_rm.action.RMActionExecuterAbstractBase; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.surf.util.I18NUtil; + +/** + * + * Edit review as of date action + * + * @author Roy Wetherall + */ +public class EditReviewAsOfDateAction extends RMActionExecuterAbstractBase +{ + /** I18N */ + private static final String MSG_SPECIFY_VALID_DATE = "rm.action.specify-avlid-date"; + private static final String MSG_REVIEW_DETAILS_ONLY = "rm.action.review-details-only"; + + @SuppressWarnings("unused") + private static Log logger = LogFactory.getLog(EditReviewAsOfDateAction.class); + + public static final String PARAM_AS_OF_DATE = "asOfDate"; + + /** + * @see org.alfresco.repo.action.executer.ActionExecuterAbstractBase#executeImpl(org.alfresco.service.cmr.action.Action, + * org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + protected void executeImpl(Action action, NodeRef actionedUponNodeRef) + { + if (recordsManagementService.isRecord(actionedUponNodeRef) == true && + this.nodeService.hasAspect(actionedUponNodeRef, ASPECT_VITAL_RECORD) == true) + { + // Get the action parameter + Date reviewAsOf = (Date)action.getParameterValue(PARAM_AS_OF_DATE); + if (reviewAsOf == null) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_SPECIFY_VALID_DATE)); + } + + // Set the as of date + this.nodeService.setProperty(actionedUponNodeRef, PROP_REVIEW_AS_OF, reviewAsOf); + + } + } + + /** + * + * @see org.alfresco.repo.action.ParameterizedItemAbstractBase#addParameterDefinitions(java.util.List) + */ + @Override + protected void addParameterDefinitions(List paramList) + { + // Intentionally empty + } + + @Override + public Set getProtectedProperties() + { + HashSet qnames = new HashSet(); + qnames.add(PROP_REVIEW_AS_OF); + return qnames; + } + + @Override + protected boolean isExecutableImpl(NodeRef filePlanComponent, Map parameters, boolean throwException) + { + boolean result = false; + if (recordsManagementService.isRecord(filePlanComponent) == true && + this.nodeService.hasAspect(filePlanComponent, ASPECT_VITAL_RECORD) == true) + { + result = true; + } + else + { + if (throwException == true) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_REVIEW_DETAILS_ONLY)); + } + } + return result; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/FileAction.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/FileAction.java new file mode 100644 index 0000000000..5a0e0cc058 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/FileAction.java @@ -0,0 +1,169 @@ +/* + * 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.module.org_alfresco_module_rm.action.impl; + +import java.io.Serializable; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.module.org_alfresco_module_rm.action.RMActionExecuterAbstractBase; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.module.org_alfresco_module_rm.vital.VitalRecordDefinition; +import org.alfresco.repo.action.ParameterDefinitionImpl; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; + +/** + * Files a record into a particular record folder + * + * @author Roy Wetherall + */ +public class FileAction extends RMActionExecuterAbstractBase +{ + /** Parameter names */ + public static final String PARAM_RECORD_METADATA_ASPECTS = "recordMetadataAspects"; + + /** + * @see org.alfresco.repo.action.executer.ActionExecuterAbstractBase#executeImpl(org.alfresco.service.cmr.action.Action, org.alfresco.service.cmr.repository.NodeRef) + */ + @SuppressWarnings("unchecked") + @Override + protected void executeImpl(Action action, NodeRef actionedUponNodeRef) + { + // Permissions perform the following checks so this action doesn't need to. + // + // check the record is within a folder + // check that the folder we are filing into is not closed + + // Get the optional list of record meta-data aspects + List recordMetadataAspects = (List)action.getParameterValue(PARAM_RECORD_METADATA_ASPECTS); + + // Add the record aspect (doesn't matter if it is already present) + if (nodeService.hasAspect(actionedUponNodeRef, ASPECT_RECORD) == false) + { + nodeService.addAspect(actionedUponNodeRef, RecordsManagementModel.ASPECT_RECORD, null); + } + + // Get the records properties + Map recordProperties = this.nodeService.getProperties(actionedUponNodeRef); + + Calendar fileCalendar = Calendar.getInstance(); + if (recordProperties.get(RecordsManagementModel.PROP_IDENTIFIER) == null) + { + // Calculate the filed date and record identifier + String year = Integer.toString(fileCalendar.get(Calendar.YEAR)); + QName nodeDbid = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "node-dbid"); + String recordId = year + "-" + padString(recordProperties.get(nodeDbid).toString(), 10); + recordProperties.put(RecordsManagementModel.PROP_IDENTIFIER, recordId); + } + + // Update/set the date this record was refiled/filed + recordProperties.put(RecordsManagementModel.PROP_DATE_FILED, fileCalendar.getTime()); + + // Set the record properties + this.nodeService.setProperties(actionedUponNodeRef, recordProperties); + + // Apply any record meta-data aspects + if (recordMetadataAspects != null && recordMetadataAspects.size() != 0) + { + for (QName aspect : recordMetadataAspects) + { + nodeService.addAspect(actionedUponNodeRef, aspect, null); + } + } + + // Calculate the review schedule + VitalRecordDefinition viDef = vitalRecordService.getVitalRecordDefinition(actionedUponNodeRef); + if (viDef != null && viDef.isEnabled() == true) + { + Date reviewAsOf = viDef.getNextReviewDate(); + if (reviewAsOf != null) + { + Map reviewProps = new HashMap(1); + reviewProps.put(RecordsManagementModel.PROP_REVIEW_AS_OF, reviewAsOf); + + if (!nodeService.hasAspect(actionedUponNodeRef, ASPECT_VITAL_RECORD)) + { + this.nodeService.addAspect(actionedUponNodeRef, RecordsManagementModel.ASPECT_VITAL_RECORD, reviewProps); + } + else + { + Map props = nodeService.getProperties(actionedUponNodeRef); + props.putAll(reviewProps); + nodeService.setProperties(actionedUponNodeRef, props); + } + } + } + + // Get the disposition instructions for the actioned upon record + DispositionSchedule di = this.dispositionService.getDispositionSchedule(actionedUponNodeRef); + + // Set up the disposition schedule if the dispositions are being managed at the record level + if (di != null && di.isRecordLevelDisposition() == true) + { + // Setup the next disposition action + updateNextDispositionAction(actionedUponNodeRef); + } + } + + /** + * @see org.alfresco.repo.action.ParameterizedItemAbstractBase#addParameterDefinitions(java.util.List) + */ + @Override + protected void addParameterDefinitions(List paramList) + { + // No parameters + paramList.add(new ParameterDefinitionImpl(PARAM_RECORD_METADATA_ASPECTS, DataTypeDefinition.QNAME, false, "Record Metadata Aspects", true)); + } + + @Override + public Set getProtectedAspects() + { + HashSet qnames = new HashSet(); + qnames.add(ASPECT_RECORD); + qnames.add(ASPECT_VITAL_RECORD); + return qnames; + } + + @Override + public Set getProtectedProperties() + { + HashSet qnames = new HashSet(); + qnames.add(PROP_DATE_FILED); + qnames.add(PROP_REVIEW_AS_OF); + return qnames; + } + + @Override + protected boolean isExecutableImpl(NodeRef filePlanComponent, Map parameters, boolean throwException) + { + return true; + } + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/FreezeAction.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/FreezeAction.java new file mode 100644 index 0000000000..2ceb3da826 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/FreezeAction.java @@ -0,0 +1,225 @@ +/* + * 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.module.org_alfresco_module_rm.action.impl; + +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.action.RMActionExecuterAbstractBase; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.transaction.AlfrescoTransactionSupport; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.surf.util.I18NUtil; + +/** + * Freeze Action + * + * @author Roy Wetherall + */ +public class FreezeAction extends RMActionExecuterAbstractBase +{ + private static final String MSG_FREEZE_NO_REASON = "rm.action.freeze-no-reason"; + private static final String MSG_FREEZE_ONLY_RECORDS_FOLDERS = "rm.action.freeze-only-records-folders"; + + /** Logger */ + private static Log logger = LogFactory.getLog(FreezeAction.class); + + /** Parameter names */ + public static final String PARAM_REASON = "reason"; + + /** Hold node reference key */ + private static final String KEY_HOLD_NODEREF = "holdNodeRef"; + + /** + * @see org.alfresco.repo.action.executer.ActionExecuterAbstractBase#executeImpl(org.alfresco.service.cmr.action.Action, org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + protected void executeImpl(Action action, NodeRef actionedUponNodeRef) + { + final boolean isRecord = recordsManagementService.isRecord(actionedUponNodeRef); + final boolean isFolder = this.recordsManagementService.isRecordFolder(actionedUponNodeRef); + + if (isRecord || isFolder) + { + // Get the property values + String reason = (String)action.getParameterValue(PARAM_REASON); + if (reason == null || reason.length() == 0) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_FREEZE_NO_REASON)); + } + + if (logger.isDebugEnabled()) + { + StringBuilder msg = new StringBuilder(); + msg.append("Freezing node ").append(actionedUponNodeRef); + if (isFolder) + { + msg.append(" (folder)"); + } + msg.append(" with reason '").append(reason).append("'"); + logger.debug(msg.toString()); + } + + // Get the root rm node + NodeRef root = this.recordsManagementService.getFilePlan(actionedUponNodeRef); + + // Get the hold object + NodeRef holdNodeRef = (NodeRef)AlfrescoTransactionSupport.getResource(KEY_HOLD_NODEREF); + + if (holdNodeRef == null) + { + // Calculate a transfer name + QName nodeDbid = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "node-dbid"); + Long dbId = (Long)this.nodeService.getProperty(actionedUponNodeRef, nodeDbid); + String transferName = padString(dbId.toString(), 10); + + // Create the hold object + Map holdProps = new HashMap(2); + holdProps.put(ContentModel.PROP_NAME, transferName); + holdProps.put(PROP_HOLD_REASON, reason); + final QName transferQName = QName.createQName(RM_URI, transferName); + holdNodeRef = this.nodeService.createNode(root, + ASSOC_HOLDS, + transferQName, + TYPE_HOLD, + holdProps).getChildRef(); + + if (logger.isDebugEnabled()) + { + StringBuilder msg = new StringBuilder(); + msg.append("Created hold object ").append(holdNodeRef) + .append(" with transfer name ").append(transferQName); + logger.debug(msg.toString()); + } + + // Bind the hold node reference to the transaction + AlfrescoTransactionSupport.bindResource(KEY_HOLD_NODEREF, holdNodeRef); + } + + // Link the record to the hold + this.nodeService.addChild( holdNodeRef, + actionedUponNodeRef, + ASSOC_FROZEN_RECORDS, + ASSOC_FROZEN_RECORDS); + + // Apply the freeze aspect + Map props = new HashMap(2); + props.put(PROP_FROZEN_AT, new Date()); + props.put(PROP_FROZEN_BY, AuthenticationUtil.getFullyAuthenticatedUser()); + this.nodeService.addAspect(actionedUponNodeRef, ASPECT_FROZEN, props); + + if (logger.isDebugEnabled()) + { + StringBuilder msg = new StringBuilder(); + msg.append("Frozen aspect applied to ").append(actionedUponNodeRef); + logger.debug(msg.toString()); + } + + + // Mark all the folders contents as frozen + if (isFolder) + { + List records = this.recordsManagementService.getRecords(actionedUponNodeRef); + for (NodeRef record : records) + { + this.nodeService.addAspect(record, ASPECT_FROZEN, props); + + if (logger.isDebugEnabled()) + { + StringBuilder msg = new StringBuilder(); + msg.append("Frozen aspect applied to ").append(record); + logger.debug(msg.toString()); + } + } + } + } + else + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_FREEZE_ONLY_RECORDS_FOLDERS)); + } + } + + @Override + public Set getProtectedAspects() + { + HashSet qnames = new HashSet(); + qnames.add(ASPECT_FROZEN); + return qnames; + } + + @Override + public Set getProtectedProperties() + { + HashSet qnames = new HashSet(); + qnames.add(PROP_HOLD_REASON); + //TODO Add prop frozen at/by? + return qnames; + } + + @Override + protected boolean isExecutableImpl(NodeRef filePlanComponent, Map parameters, boolean throwException) + { + if (this.recordsManagementService.isRecord(filePlanComponent) == true || + this.recordsManagementService.isRecordFolder(filePlanComponent) == true) + { + // Get the property values + if(parameters != null) + { + String reason = (String)parameters.get(PARAM_REASON); + if (reason == null || reason.length() == 0) + { + if(throwException) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_FREEZE_NO_REASON)); + } + else + { + return false; + } + } + } + return true; + } + else + { + if(throwException) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_FREEZE_ONLY_RECORDS_FOLDERS)); + } + else + { + return false; + } + } + } + + +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/OpenRecordFolderAction.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/OpenRecordFolderAction.java new file mode 100644 index 0000000000..7cda1b9960 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/OpenRecordFolderAction.java @@ -0,0 +1,104 @@ +/* + * 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.module.org_alfresco_module_rm.action.impl; + +import java.io.Serializable; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.module.org_alfresco_module_rm.action.RMActionExecuterAbstractBase; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.surf.util.I18NUtil; + +/** + * Action to re-open the records folder + * + * @author Roy Wetherall + */ +public class OpenRecordFolderAction extends RMActionExecuterAbstractBase +{ + private static final String MSG_NO_OPEN_RECORD_FOLDER = "rm.action.no-open-record-folder"; + + /** + * @see org.alfresco.repo.action.executer.ActionExecuterAbstractBase#executeImpl(org.alfresco.service.cmr.action.Action, + * org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + protected void executeImpl(Action action, NodeRef actionedUponNodeRef) + { + // TODO check that the user in question has the correct permission to re-open a records folder + + if (this.recordsManagementService.isRecordFolder(actionedUponNodeRef) == true) + { + Boolean isClosed = (Boolean) this.nodeService.getProperty(actionedUponNodeRef, PROP_IS_CLOSED); + if (Boolean.TRUE.equals(isClosed) == true) + { + this.nodeService.setProperty(actionedUponNodeRef, PROP_IS_CLOSED, false); + } + } + else + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_NO_OPEN_RECORD_FOLDER, actionedUponNodeRef.toString())); + } + } + + /** + * @see org.alfresco.repo.action.ParameterizedItemAbstractBase#addParameterDefinitions(java.util.List) + */ + @Override + protected void addParameterDefinitions(List paramList) + { + // TODO Auto-generated method stub + } + + @Override + public Set getProtectedProperties() + { + HashSet qnames = new HashSet(); + qnames.add(PROP_IS_CLOSED); + return qnames; + } + + @Override + protected boolean isExecutableImpl(NodeRef filePlanComponent, Map parameters, boolean throwException) + { + if (this.recordsManagementService.isRecordFolder(filePlanComponent) == true) + { + return true; + } + else + { + if (throwException) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_NO_OPEN_RECORD_FOLDER, filePlanComponent.toString())); + } + else + { + return false; + } + } + } + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/RelinquishHoldAction.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/RelinquishHoldAction.java new file mode 100644 index 0000000000..5fdb6a9676 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/RelinquishHoldAction.java @@ -0,0 +1,200 @@ +/* + * 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.module.org_alfresco_module_rm.action.impl; + +import java.io.Serializable; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.action.RMActionExecuterAbstractBase; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.surf.util.I18NUtil; + +/** + * Relinquish Hold Action + * + * @author Roy Wetherall + */ +public class RelinquishHoldAction extends RMActionExecuterAbstractBase +{ + /** Logger */ + private static Log logger = LogFactory.getLog(RelinquishHoldAction.class); + + /** I18N */ + private static final String MSG_NOT_HOLD_TYPE = "rm.action.not-hold-type"; + + /** + * @see org.alfresco.repo.action.executer.ActionExecuterAbstractBase#executeImpl(org.alfresco.service.cmr.action.Action, org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + protected void executeImpl(Action action, NodeRef actionedUponNodeRef) + { + QName nodeType = this.nodeService.getType(actionedUponNodeRef); + if (this.dictionaryService.isSubClass(nodeType, TYPE_HOLD) == true) + { + final NodeRef holdBeingRelinquished = actionedUponNodeRef; + List frozenNodeAssocs = nodeService.getChildAssocs(holdBeingRelinquished, ASSOC_FROZEN_RECORDS, RegexQNamePattern.MATCH_ALL); + + if (logger.isDebugEnabled()) + { + StringBuilder msg = new StringBuilder(); + msg.append("Relinquishing hold ").append(holdBeingRelinquished) + .append(" which has ").append(frozenNodeAssocs.size()).append(" frozen node(s)."); + logger.debug(msg.toString()); + } + + for (ChildAssociationRef assoc : frozenNodeAssocs) + { + final NodeRef nextFrozenNode = assoc.getChildRef(); + + // Remove the freeze if this is the only hold that references the node + removeFreeze(nextFrozenNode, holdBeingRelinquished); + } + + if (logger.isDebugEnabled()) + { + StringBuilder msg = new StringBuilder(); + msg.append("Deleting hold object ").append(holdBeingRelinquished) + .append(" with name ").append(nodeService.getProperty(holdBeingRelinquished, ContentModel.PROP_NAME)); + logger.debug(msg.toString()); + } + + // Delete the hold node + this.nodeService.deleteNode(holdBeingRelinquished); + } + else + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_NOT_HOLD_TYPE, TYPE_HOLD.toString(), actionedUponNodeRef.toString())); + } + } + + /** + * Removes a freeze from a node + * + * @param nodeRef node reference + */ + private void removeFreeze(NodeRef nodeRef, NodeRef holdBeingRelinquished) + { + // We should only remove the frozen aspect if there are no other 'holds' in effect for this node. + // One complication to consider is that holds can be placed on records or on folders. + // Therefore if the nodeRef here is a record, we need to go up the containment hierarchy looking + // for holds at each level. + + // Get all the holds and remove this node from them. + List parentAssocs = this.nodeService.getParentAssocs(nodeRef, ASSOC_FROZEN_RECORDS, RegexQNamePattern.MATCH_ALL); + // If the nodeRef is a record, there could also be applicable holds as parents of the folder(s). + if (recordsManagementService.isRecord(nodeRef)) + { + List parentFolders = recordsManagementService.getRecordFolders(nodeRef); + for (NodeRef folder : parentFolders) + { + List moreAssocs = nodeService.getParentAssocs(folder, ASSOC_FROZEN_RECORDS, RegexQNamePattern.MATCH_ALL); + parentAssocs.addAll(moreAssocs); + } + } + + if (logger.isDebugEnabled()) + { + StringBuilder msg = new StringBuilder(); + msg.append("Removing freeze from ").append(nodeRef).append(" which has ") + .append(parentAssocs.size()).append(" holds"); + logger.debug(msg.toString()); + } + + boolean otherHoldsAreInEffect = false; + for (ChildAssociationRef chAssRef : parentAssocs) + { + if (!chAssRef.getParentRef().equals(holdBeingRelinquished)) + { + otherHoldsAreInEffect = true; + break; + } + } + + if (!otherHoldsAreInEffect) + { + if (logger.isDebugEnabled()) + { + StringBuilder msg = new StringBuilder(); + msg.append("Removing frozen aspect from ").append(nodeRef); + logger.debug(msg.toString()); + } + + // Remove the aspect + this.nodeService.removeAspect(nodeRef, ASPECT_FROZEN); + } + + // Remove the freezes on the child records as long as there is no other hold referencing them + if (this.recordsManagementService.isRecordFolder(nodeRef) == true) + { + if (logger.isDebugEnabled()) + { + StringBuilder msg = new StringBuilder(); + msg.append(nodeRef).append(" is a record folder"); + logger.debug(msg.toString()); + } + for (NodeRef record : recordsManagementService.getRecords(nodeRef)) + { + removeFreeze(record, holdBeingRelinquished); + } + } + + } + + @Override + public Set getProtectedAspects() + { + HashSet qnames = new HashSet(); + qnames.add(ASPECT_FROZEN); + return qnames; + } + + @Override + protected boolean isExecutableImpl(NodeRef filePlanComponent, Map parameters, boolean throwException) + { + QName nodeType = this.nodeService.getType(filePlanComponent); + if (this.dictionaryService.isSubClass(nodeType, TYPE_HOLD) == true) + { + return true; + } + else + { + if(throwException) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_NOT_HOLD_TYPE, TYPE_HOLD.toString(), filePlanComponent.toString())); + } + else + { + return false; + } + } + } + + +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/RetainAction.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/RetainAction.java new file mode 100644 index 0000000000..f8577ff6fe --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/RetainAction.java @@ -0,0 +1,43 @@ +/* + * 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.module.org_alfresco_module_rm.action.impl; + +import org.alfresco.module.org_alfresco_module_rm.action.RMDispositionActionExecuterAbstractBase; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Retain action + * + * @author Roy Wetherall + */ +public class RetainAction extends RMDispositionActionExecuterAbstractBase +{ + @Override + protected void executeRecordFolderLevelDisposition(Action action, NodeRef recordFolder) + { + // Do nothing + } + + @Override + protected void executeRecordLevelDisposition(Action action, NodeRef record) + { + // Do nothing + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/SetupRecordFolderAction.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/SetupRecordFolderAction.java new file mode 100644 index 0000000000..052a379479 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/SetupRecordFolderAction.java @@ -0,0 +1,83 @@ +/* + * 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.module.org_alfresco_module_rm.action.impl; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.action.RMActionExecuterAbstractBase; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule; +import org.alfresco.module.org_alfresco_module_rm.vital.VitalRecordDefinition; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + +/** + * Action to close the records folder + * + * @author Roy Wetherall + */ +public class SetupRecordFolderAction extends RMActionExecuterAbstractBase +{ + /** + * @see org.alfresco.repo.action.executer.ActionExecuterAbstractBase#executeImpl(org.alfresco.service.cmr.action.Action, org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + protected void executeImpl(Action action, NodeRef actionedUponNodeRef) + { + if (this.recordsManagementService.isRecordFolder(actionedUponNodeRef) == true) + { + // Inherit the vital record details + VitalRecordDefinition vrDef = vitalRecordService.getVitalRecordDefinition(actionedUponNodeRef); + Map props = new HashMap(2); + if (vrDef != null) + { + props.put(PROP_VITAL_RECORD_INDICATOR, vrDef.isEnabled()); + props.put(PROP_REVIEW_PERIOD, vrDef.getReviewPeriod()); + } + this.nodeService.addAspect(actionedUponNodeRef, ASPECT_VITAL_RECORD_DEFINITION, props); + + // Set up the disposition schedule if the dispositions are being managed at the folder level + DispositionSchedule di = this.dispositionService.getDispositionSchedule(actionedUponNodeRef); + if (di != null && di.isRecordLevelDisposition() == false) + { + // Setup the next disposition action + updateNextDispositionAction(actionedUponNodeRef); + } + } + } + + /** + * @see org.alfresco.repo.action.ParameterizedItemAbstractBase#addParameterDefinitions(java.util.List) + */ + @Override + protected void addParameterDefinitions(List paramList) + { + } + + @Override + protected boolean isExecutableImpl(NodeRef filePlanComponent, Map parameters, boolean throwException) + { + return true; + } + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/SplitEmailAction.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/SplitEmailAction.java new file mode 100644 index 0000000000..74f6737eb7 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/SplitEmailAction.java @@ -0,0 +1,326 @@ +/* + * 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.module.org_alfresco_module_rm.action.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Serializable; +import java.io.UnsupportedEncodingException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.mail.MessagingException; +import javax.mail.Multipart; +import javax.mail.Part; +import javax.mail.internet.ContentType; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeUtility; +import javax.transaction.UserTransaction; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.model.ImapModel; +import org.alfresco.module.org_alfresco_module_rm.action.RMActionExecuterAbstractBase; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.dictionary.AssociationDefinition; +import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.surf.util.I18NUtil; +import org.springframework.util.FileCopyUtils; + +/** + * Split Email Action + * + * Splits the attachments for an email message out to independent records. + * + * @author Mark Rogers + */ +public class SplitEmailAction extends RMActionExecuterAbstractBase +{ + /** I18N */ + private static final String MSG_NO_READ_MIME_MESSAGE = "rm.action.no-read-mime-message"; + private static final String MSG_EMAIL_DECLARED = "rm.action.email-declared"; + private static final String MSG_EMAIL_NOT_RECORD = "rm.action.email-not-record"; + private static final String MSG_EMAIL_CREATE_CHILD_ASSOC = "rm.action.email-create-child-assoc"; + + /** Logger */ + private static Log logger = LogFactory.getLog(SplitEmailAction.class); + + /** + * @see org.alfresco.repo.action.executer.ActionExecuterAbstractBase#executeImpl(org.alfresco.service.cmr.action.Action, + * org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + protected void executeImpl(Action action, NodeRef actionedUponNodeRef) + { + // get node type + nodeService.getType(actionedUponNodeRef); + + if (logger.isDebugEnabled() == true) + { + logger.debug("split email:" + actionedUponNodeRef); + } + + if (recordsManagementService.isRecord(actionedUponNodeRef) == true) + { + if (recordsManagementService.isRecordDeclared(actionedUponNodeRef) == false) + { + ChildAssociationRef parent = nodeService.getPrimaryParent(actionedUponNodeRef); + + /** + * Check whether the email message has already been split - do nothing if it has already been split + */ + List refs = nodeService.getTargetAssocs(actionedUponNodeRef, ImapModel.ASSOC_IMAP_ATTACHMENT); + if(refs.size() > 0) + { + if (logger.isDebugEnabled() == true) + { + logger.debug("mail message has already been split - do nothing"); + } + return; + } + + /** + * Get the content and if its a mime message then create atachments for each part + */ + try + { + ContentReader reader = contentService.getReader(actionedUponNodeRef, ContentModel.PROP_CONTENT); + InputStream is = reader.getContentInputStream(); + MimeMessage mimeMessage = new MimeMessage(null, is); + Object content = mimeMessage.getContent(); + if (content instanceof Multipart) + { + Multipart multipart = (Multipart)content; + + for (int i = 0, n = multipart.getCount(); i < n; i++) + { + Part part = multipart.getBodyPart(i); + if ("attachment".equalsIgnoreCase(part.getDisposition())) + { + createAttachment(actionedUponNodeRef, parent.getParentRef(), part); + } + } + } + } + catch (Exception e) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_NO_READ_MIME_MESSAGE, e.toString()), e); + } + } + else + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_EMAIL_DECLARED, actionedUponNodeRef.toString())); + } + } + else + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_EMAIL_NOT_RECORD, actionedUponNodeRef.toString())); + } + } + + @Override + protected boolean isExecutableImpl(NodeRef filePlanComponent, Map parameters, boolean throwException) + { + if (recordsManagementService.isRecord(filePlanComponent) == true) + { + if (recordsManagementService.isRecordDeclared(filePlanComponent)) + { + if (throwException) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_EMAIL_DECLARED, filePlanComponent.toString())); + } + else + { + return false; + } + } + } + else + { + if (throwException) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_EMAIL_NOT_RECORD, filePlanComponent.toString())); + } + else + { + return false; + } + } + return true; + } + + /** + * Create attachment from Mime Message Part + * @param messageNodeRef - the node ref of the mime message + * @param parentNodeRef - the node ref of the parent folder + * @param part + * @throws MessagingException + * @throws IOException + */ + private void createAttachment(NodeRef messageNodeRef, NodeRef parentNodeRef, Part part) throws MessagingException, IOException + { + String fileName = part.getFileName(); + try + { + fileName = MimeUtility.decodeText(fileName); + } + catch (UnsupportedEncodingException e) + { + if (logger.isWarnEnabled()) + { + logger.warn("Cannot decode file name '" + fileName + "'", e); + } + } + + Map messageProperties = nodeService.getProperties(messageNodeRef); + String messageTitle = (String)messageProperties.get(ContentModel.PROP_NAME); + if(messageTitle == null) + { + messageTitle = fileName; + } + else + { + messageTitle = messageTitle + " - " + fileName; + } + + ContentType contentType = new ContentType(part.getContentType()); + + Map docProps = new HashMap(1); + docProps.put(ContentModel.PROP_NAME, messageTitle + " - " + fileName); + docProps.put(ContentModel.PROP_TITLE, fileName); + + /** + * Create an attachment node in the same folder as the message + */ + ChildAssociationRef attachmentRef = nodeService.createNode(parentNodeRef, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, fileName), + ContentModel.TYPE_CONTENT, + docProps); + + /** + * Write the content into the new attachment node + */ + ContentWriter writer = contentService.getWriter(attachmentRef.getChildRef(), ContentModel.PROP_CONTENT, true); + writer.setMimetype(contentType.getBaseType()); + OutputStream os = writer.getContentOutputStream(); + FileCopyUtils.copy(part.getInputStream(), os); + + /** + * Create a link from the message to the attachment + */ + createRMReference(messageNodeRef, attachmentRef.getChildRef()); + + + } + + QName assocDef = null; + + /** + * Create a link from the message to the attachment + */ + private void createRMReference(NodeRef parentRef, NodeRef childRef) + { + String sourceId = "message"; + String targetId = "attachment"; + + String compoundId = recordsManagementAdminService.getCompoundIdFor(sourceId, targetId); + + Map refs = recordsManagementAdminService.getCustomReferenceDefinitions(); + for(QName name : refs.keySet()) + { + // TODO how to find assocDef? + // Refs seems to be null + } + + if(assocDef == null) + { + assocDef = createReference(); + } + + recordsManagementAdminService.addCustomReference(parentRef, childRef, assocDef); + + // add the IMAP attachment aspect + nodeService.createAssociation( + parentRef, + childRef, + ImapModel.ASSOC_IMAP_ATTACHMENT); + } + + /** + * Create the custom reference - need to jump through hoops with the transaction handling here + * since the association is created in the post commit phase, so it can't be used within the + * current transaction. + * + * @return + */ + private QName createReference() + { + UserTransaction txn = null; + + try + { + txn = transactionService.getNonPropagatingUserTransaction(); + txn.begin(); + RetryingTransactionHelper helper = transactionService.getRetryingTransactionHelper(); + RetryingTransactionCallback addCustomChildAssocDefinitionCallback = new RetryingTransactionCallback() + { + public QName execute() throws Throwable + { + String sourceId = "message"; + String targetId = "attachment"; + QName assocDef = recordsManagementAdminService.addCustomChildAssocDefinition(sourceId, targetId); + return assocDef; + } + }; + QName ret = helper.doInTransaction(addCustomChildAssocDefinitionCallback); + + txn.commit(); + return ret; + } + catch (Exception e) + { + if(txn != null) + { + try + { + txn.rollback(); + } + catch (Exception se) + { + logger.error("error during creation of custom child association", se); + // we can do nothing with this rollback exception. + } + } + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_EMAIL_CREATE_CHILD_ASSOC), e); + } + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/TransferAction.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/TransferAction.java new file mode 100644 index 0000000000..57e8818b59 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/TransferAction.java @@ -0,0 +1,215 @@ +/* + * 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.module.org_alfresco_module_rm.action.impl; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.action.RMDispositionActionExecuterAbstractBase; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionAction; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionActionDefinition; +import org.alfresco.repo.action.executer.ActionExecuter; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.transaction.AlfrescoTransactionSupport; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.surf.util.I18NUtil; + +/** + * Transfer action + * + * @author Roy Wetherall + */ +public class TransferAction extends RMDispositionActionExecuterAbstractBase +{ + /** Transfer node reference key */ + public static final String KEY_TRANSFER_NODEREF = "transferNodeRef"; + + /** I18N */ + public static final String MSG_NODE_ALREADY_TRANSFER = "rm.action.node-already-transfer"; + + /** Indicates whether the transfer is an accession or not */ + private boolean isAccession = false; + + /** + * Indicates whether this transfer is an accession or not + * + * @param isAccession + */ + public void setIsAccession(boolean isAccession) + { + this.isAccession = isAccession; + } + + /** + * Do not set the transfer action to auto-complete + * + * @see org.alfresco.module.org_alfresco_module_rm.action.RMDispositionActionExecuterAbstractBase#getSetDispositionActionComplete() + */ + @Override + public boolean getSetDispositionActionComplete() + { + return false; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.action.RMDispositionActionExecuterAbstractBase#executeRecordFolderLevelDisposition(org.alfresco.service.cmr.action.Action, org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + protected void executeRecordFolderLevelDisposition(Action action, NodeRef recordFolder) + { + doTransfer(action, recordFolder); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.action.RMDispositionActionExecuterAbstractBase#executeRecordLevelDisposition(org.alfresco.service.cmr.action.Action, org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + protected void executeRecordLevelDisposition(Action action, NodeRef record) + { + doTransfer(action, record); + } + + /** + * Create the transfer node and link the disposition lifecycle node beneath it + * + * @param dispositionLifeCycleNodeRef disposition lifecycle node + */ + private void doTransfer(Action action, NodeRef dispositionLifeCycleNodeRef) + { + // Get the root rm node + NodeRef root = this.recordsManagementService.getFilePlan(dispositionLifeCycleNodeRef); + + // Get the hold object + NodeRef transferNodeRef = (NodeRef)AlfrescoTransactionSupport.getResource(KEY_TRANSFER_NODEREF); + if (transferNodeRef == null) + { + // Calculate a transfer name + QName nodeDbid = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "node-dbid"); + Long dbId = (Long)this.nodeService.getProperty(dispositionLifeCycleNodeRef, nodeDbid); + String transferName = padString(dbId.toString(), 10); + + // Create the transfer object + Map transferProps = new HashMap(2); + transferProps.put(ContentModel.PROP_NAME, transferName); + transferProps.put(PROP_TRANSFER_ACCESSION_INDICATOR, this.isAccession); + + // setup location property from disposition schedule + DispositionAction da = dispositionService.getNextDispositionAction(dispositionLifeCycleNodeRef); + if (da != null) + { + DispositionActionDefinition actionDef = da.getDispositionActionDefinition(); + if (actionDef != null) + { + transferProps.put(PROP_TRANSFER_LOCATION, actionDef.getLocation()); + } + } + + transferNodeRef = this.nodeService.createNode(root, + ASSOC_TRANSFERS, + QName.createQName(RM_URI, transferName), + TYPE_TRANSFER, + transferProps).getChildRef(); + + // Bind the hold node reference to the transaction + AlfrescoTransactionSupport.bindResource(KEY_TRANSFER_NODEREF, transferNodeRef); + } + + // Link the record to the hold + this.nodeService.addChild(transferNodeRef, + dispositionLifeCycleNodeRef, + ASSOC_TRANSFERRED, + ASSOC_TRANSFERRED); + + // Set PDF indicator flag + setPDFIndicationFlag(transferNodeRef, dispositionLifeCycleNodeRef); + + // Set the return value of the action + action.setParameterValue(ActionExecuter.PARAM_RESULT, transferNodeRef); + } + + /** + * + * @param transferNodeRef + * @param dispositionLifeCycleNodeRef + */ + private void setPDFIndicationFlag(NodeRef transferNodeRef, NodeRef dispositionLifeCycleNodeRef) + { + if (recordsManagementService.isRecordFolder(dispositionLifeCycleNodeRef) == true) + { + List records = recordsManagementService.getRecords(dispositionLifeCycleNodeRef); + for (NodeRef record : records) + { + setPDFIndicationFlag(transferNodeRef, record); + } + } + else + { + ContentData contentData = (ContentData)nodeService.getProperty(dispositionLifeCycleNodeRef, ContentModel.PROP_CONTENT); + if (contentData != null && + MimetypeMap.MIMETYPE_PDF.equals(contentData.getMimetype()) == true) + { + // Set the property indicator + nodeService.setProperty(transferNodeRef, PROP_TRANSFER_PDF_INDICATOR, true); + } + } + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.action.RMDispositionActionExecuterAbstractBase#isExecutableImpl(org.alfresco.service.cmr.repository.NodeRef, java.util.Map, boolean) + */ + @Override + protected boolean isExecutableImpl(NodeRef filePlanComponent, Map parameters, boolean throwException) + { + + if(!super.isExecutableImpl(filePlanComponent, parameters, throwException)) + { + // super will throw ... + return false; + } + NodeRef transferNodeRef = (NodeRef)AlfrescoTransactionSupport.getResource(KEY_TRANSFER_NODEREF); + if (transferNodeRef != null) + { + List transferredAlready = nodeService.getChildAssocs(transferNodeRef, ASSOC_TRANSFERRED, ASSOC_TRANSFERRED); + for(ChildAssociationRef car : transferredAlready) + { + if(car.getChildRef().equals(filePlanComponent)) + { + if (throwException) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_NODE_ALREADY_TRANSFER, filePlanComponent.toString())); + } + else + { + return false; + } + } + } + } + return true; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/TransferCompleteAction.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/TransferCompleteAction.java new file mode 100644 index 0000000000..4f7f2ba65f --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/TransferCompleteAction.java @@ -0,0 +1,144 @@ +/* + * 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.module.org_alfresco_module_rm.action.impl; + +import java.io.Serializable; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.module.org_alfresco_module_rm.action.RMActionExecuterAbstractBase; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionAction; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.transaction.AlfrescoTransactionSupport; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.springframework.extensions.surf.util.I18NUtil; + +/** + * Transfer complete action + * + * @author Roy Wetherall + */ +public class TransferCompleteAction extends RMActionExecuterAbstractBase +{ + /** I18N */ + private static final String MSG_NODE_NOT_TRANSFER = "rm.action.node-not-transfer"; + + /** + * @see org.alfresco.module.org_alfresco_module_rm.action.RMActionExecuterAbstractBase#isExecutableImpl(org.alfresco.service.cmr.repository.NodeRef, + * java.util.Map, boolean) + */ + @Override + protected boolean isExecutableImpl(NodeRef filePlanComponent, Map parameters, boolean throwException) + { + QName className = this.nodeService.getType(filePlanComponent); + if (this.dictionaryService.isSubClass(className, TYPE_TRANSFER) == true) + { + return true; + } + else + { + List assocs = this.nodeService.getParentAssocs(filePlanComponent, ASSOC_TRANSFERRED, RegexQNamePattern.MATCH_ALL); + return assocs.size() > 0; + } + } + + /** + * @see org.alfresco.repo.action.executer.ActionExecuterAbstractBase#executeImpl(org.alfresco.service.cmr.action.Action, + * org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + protected void executeImpl(Action action, NodeRef actionedUponNodeRef) + { + QName className = this.nodeService.getType(actionedUponNodeRef); + if (this.dictionaryService.isSubClass(className, TYPE_TRANSFER) == true) + { + boolean accessionIndicator = ((Boolean)nodeService.getProperty(actionedUponNodeRef, PROP_TRANSFER_ACCESSION_INDICATOR)).booleanValue(); + + List assocs = this.nodeService.getChildAssocs(actionedUponNodeRef, ASSOC_TRANSFERRED, RegexQNamePattern.MATCH_ALL); + for (ChildAssociationRef assoc : assocs) + { + markComplete(assoc.getChildRef(), accessionIndicator); + } + + // Delete the transfer object + this.nodeService.deleteNode(actionedUponNodeRef); + + NodeRef transferNodeRef = (NodeRef) AlfrescoTransactionSupport.getResource(TransferAction.KEY_TRANSFER_NODEREF); + if (transferNodeRef != null) + { + if (transferNodeRef.equals(actionedUponNodeRef)) + { + AlfrescoTransactionSupport.bindResource(TransferAction.KEY_TRANSFER_NODEREF, null); + } + } + } + else + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_NODE_NOT_TRANSFER)); + } + } + + /** + * Marks the node complete + * + * @param nodeRef + * disposition lifecycle node reference + */ + private void markComplete(NodeRef nodeRef, boolean accessionIndicator) + { + // Set the completed date + DispositionAction da = dispositionService.getNextDispositionAction(nodeRef); + if (da != null) + { + nodeService.setProperty(da.getNodeRef(), PROP_DISPOSITION_ACTION_COMPLETED_AT, new Date()); + nodeService.setProperty(da.getNodeRef(), PROP_DISPOSITION_ACTION_COMPLETED_BY, AuthenticationUtil.getRunAsUser()); + } + + // Determine which marker aspect to use + QName markerAspectQName = null; + if (accessionIndicator == true) + { + markerAspectQName = ASPECT_ASCENDED; + } + else + { + markerAspectQName = ASPECT_TRANSFERRED; + } + + // Mark the object and children accordingly + nodeService.addAspect(nodeRef, markerAspectQName, null); + if (recordsManagementService.isRecordFolder(nodeRef) == true) + { + List records = recordsManagementService.getRecords(nodeRef); + for (NodeRef record : records) + { + nodeService.addAspect(record, markerAspectQName, null); + } + } + + // Update to the next disposition action + updateNextDispositionAction(nodeRef); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/UnCutoffAction.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/UnCutoffAction.java new file mode 100644 index 0000000000..69eb983ada --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/UnCutoffAction.java @@ -0,0 +1,128 @@ +/* + * 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.module.org_alfresco_module_rm.action.impl; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.module.org_alfresco_module_rm.action.RMActionExecuterAbstractBase; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionAction; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.repository.NodeRef; +import org.springframework.extensions.surf.util.I18NUtil; + +/** + * UnCutoff action implementation + * + * @author Roy Wetherall + */ +public class UnCutoffAction extends RMActionExecuterAbstractBase +{ + /** I18N */ + private static final String MSG_UNDO_NOT_LAST = "rm.action.undo-not-last"; + + /** + * @see org.alfresco.repo.action.executer.ActionExecuterAbstractBase#executeImpl(org.alfresco.service.cmr.action.Action, org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + protected void executeImpl(Action action, NodeRef actionedUponNodeRef) + { + if (nodeService.hasAspect(actionedUponNodeRef, ASPECT_DISPOSITION_LIFECYCLE) == true && + nodeService.hasAspect(actionedUponNodeRef, ASPECT_CUT_OFF) == true) + { + // Get the last disposition action + DispositionAction da = dispositionService.getLastCompletedDispostionAction(actionedUponNodeRef); + + // Check that the last disposition action was a cutoff + if (da == null || da.getName().equals("cutoff") == false) + { + // Can not undo cut off since cut off was not the last thing done + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_UNDO_NOT_LAST)); + } + + // Delete the current disposition action + DispositionAction currentDa = dispositionService.getNextDispositionAction(actionedUponNodeRef); + if (currentDa != null) + { + nodeService.deleteNode(currentDa.getNodeRef()); + } + + // Move the previous (cutoff) disposition back to be current + nodeService.moveNode(da.getNodeRef(), actionedUponNodeRef, ASSOC_NEXT_DISPOSITION_ACTION, ASSOC_NEXT_DISPOSITION_ACTION); + + // Reset the started and completed property values + nodeService.setProperty(da.getNodeRef(), PROP_DISPOSITION_ACTION_STARTED_AT, null); + nodeService.setProperty(da.getNodeRef(), PROP_DISPOSITION_ACTION_STARTED_BY, null); + nodeService.setProperty(da.getNodeRef(), PROP_DISPOSITION_ACTION_COMPLETED_AT, null); + nodeService.setProperty(da.getNodeRef(), PROP_DISPOSITION_ACTION_COMPLETED_BY, null); + + // Remove the cutoff aspect + nodeService.removeAspect(actionedUponNodeRef, ASPECT_CUT_OFF); + if (recordsManagementService.isRecordFolder(actionedUponNodeRef) == true) + { + List records = this.recordsManagementService.getRecords(actionedUponNodeRef); + for (NodeRef record : records) + { + nodeService.removeAspect(record, ASPECT_CUT_OFF); + } + } + } + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.action.RMActionExecuterAbstractBase#isExecutableImpl(org.alfresco.service.cmr.repository.NodeRef, java.util.Map, boolean) + */ + @Override + protected boolean isExecutableImpl(NodeRef filePlanComponent, Map parameters, boolean throwException) + { + boolean result = true; + + if (nodeService.hasAspect(filePlanComponent, ASPECT_DISPOSITION_LIFECYCLE) == true && + nodeService.hasAspect(filePlanComponent, ASPECT_CUT_OFF) == true) + { + // Get the last disposition action + DispositionAction da = dispositionService.getLastCompletedDispostionAction(filePlanComponent); + + // Check that the last disposition action was a cutoff + if (da == null || da.getName().equals("cutoff") == false) + { + if (throwException == true) + { + // Can not undo cut off since cut off was not the last thing done + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_UNDO_NOT_LAST)); + } + result = false; + } + } + else + { + if (throwException == true) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_UNDO_NOT_LAST)); + } + result = false; + } + + return result; + } + + +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/UndeclareRecordAction.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/UndeclareRecordAction.java new file mode 100644 index 0000000000..a2a926f22d --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/UndeclareRecordAction.java @@ -0,0 +1,96 @@ +/* + * 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.module.org_alfresco_module_rm.action.impl; + +import java.io.Serializable; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.module.org_alfresco_module_rm.action.RMActionExecuterAbstractBase; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.surf.util.I18NUtil; + +/** + * Undeclare record action + * + * @author Roy Wetherall + */ +public class UndeclareRecordAction extends RMActionExecuterAbstractBase +{ + /** I18N */ + private static final String MSG_RECORDS_ONLY_UNDECLARED = "rm.action.records_only_undeclared"; + + /** + * @see org.alfresco.repo.action.executer.ActionExecuterAbstractBase#executeImpl(org.alfresco.service.cmr.action.Action, org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + protected void executeImpl(Action action, NodeRef actionedUponNodeRef) + { + if (recordsManagementService.isRecord(actionedUponNodeRef) == true) + { + if (recordsManagementService.isRecordDeclared(actionedUponNodeRef) == true) + { + // Remove the declared aspect + this.nodeService.removeAspect(actionedUponNodeRef, ASPECT_DECLARED_RECORD); + } + } + else + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_RECORDS_ONLY_UNDECLARED)); + } + } + + @Override + public Set getProtectedAspects() + { + HashSet qnames = new HashSet(); + qnames.add(ASPECT_DECLARED_RECORD); + return qnames; + } + + @Override + protected boolean isExecutableImpl(NodeRef filePlanComponent, Map parameters, boolean throwException) + { + if (recordsManagementService.isRecord(filePlanComponent) == true) + { + if (recordsManagementService.isRecordDeclared(filePlanComponent) == true) + { + return true; + } + return false; + } + else + { + if(throwException) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_RECORDS_ONLY_UNDECLARED)); + } + else + { + return false; + } + } + } + + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/UndoEventAction.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/UndoEventAction.java new file mode 100644 index 0000000000..571fe8d159 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/UndoEventAction.java @@ -0,0 +1,219 @@ +/* + * 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.module.org_alfresco_module_rm.action.impl; + +import java.io.Serializable; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.module.org_alfresco_module_rm.action.RMActionExecuterAbstractBase; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionAction; +import org.alfresco.module.org_alfresco_module_rm.event.EventCompletionDetails; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.surf.util.I18NUtil; + +/** + * Undo event action + * + * @author Roy Wetherall + */ +public class UndoEventAction extends RMActionExecuterAbstractBase +{ + /** I18N */ + private static final String MSG_EVENT_NOT_DONE = "rm.action.event-not-undone"; + + /** Params */ + public static final String PARAM_EVENT_NAME = "eventName"; + + /** + * @see org.alfresco.repo.action.executer.ActionExecuterAbstractBase#executeImpl(org.alfresco.service.cmr.action.Action, org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + protected void executeImpl(Action action, NodeRef actionedUponNodeRef) + { + String eventName = (String)action.getParameterValue(PARAM_EVENT_NAME); + + if (this.nodeService.hasAspect(actionedUponNodeRef, ASPECT_DISPOSITION_LIFECYCLE) == true) + { + // Get the next disposition action + DispositionAction da = this.dispositionService.getNextDispositionAction(actionedUponNodeRef); + if (da != null) + { + // Get the disposition event + EventCompletionDetails event = getEvent(da, eventName); + if (event != null) + { + // Update the event so that it is undone + NodeRef eventNodeRef = event.getNodeRef(); + Map props = this.nodeService.getProperties(eventNodeRef); + props.put(PROP_EVENT_EXECUTION_COMPLETE, false); + props.put(PROP_EVENT_EXECUTION_COMPLETED_AT, null); + props.put(PROP_EVENT_EXECUTION_COMPLETED_BY, null); + this.nodeService.setProperties(eventNodeRef, props); + + // Check to see if the events eligible property needs to be updated + updateEventEigible(da); + + } + else + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_EVENT_NOT_DONE, eventName)); + } + } + } + } + + /** + * Get the event from the dispostion action + * + * @param da + * @param eventName + * @return + */ + private EventCompletionDetails getEvent(DispositionAction da, String eventName) + { + EventCompletionDetails result = null; + List events = da.getEventCompletionDetails(); + for (EventCompletionDetails event : events) + { + if (eventName.equals(event.getEventName()) == true) + { + result = event; + break; + } + } + return result; + } + + /** + * + * @param da + * @param nodeRef + */ + private void updateEventEigible(DispositionAction da) + { + List events = da.getEventCompletionDetails(); + + boolean eligible = false; + if (da.getDispositionActionDefinition().eligibleOnFirstCompleteEvent() == false) + { + eligible = true; + for (EventCompletionDetails event : events) + { + if (event.isEventComplete() == false) + { + eligible = false; + break; + } + } + } + else + { + for (EventCompletionDetails event : events) + { + if (event.isEventComplete() == true) + { + eligible = true; + break; + } + } + } + + // Update the property with the eligible value + this.nodeService.setProperty(da.getNodeRef(), PROP_DISPOSITION_EVENTS_ELIGIBLE, eligible); + } + + + /** + * @see org.alfresco.repo.action.ParameterizedItemAbstractBase#addParameterDefinitions(java.util.List) + */ + @Override + protected void addParameterDefinitions(List paramList) + { + // TODO add parameter definitions .... + // eventName + + } + + @Override + public Set getProtectedAspects() + { + HashSet qnames = new HashSet(); + qnames.add(ASPECT_DISPOSITION_LIFECYCLE); + return qnames; + } + + @Override + public Set getProtectedProperties() + { + HashSet qnames = new HashSet(); + qnames.add(PROP_EVENT_EXECUTION_COMPLETE); + qnames.add(PROP_EVENT_EXECUTION_COMPLETED_AT); + qnames.add(PROP_EVENT_EXECUTION_COMPLETED_BY); + return qnames; + } + + @Override + protected boolean isExecutableImpl(NodeRef filePlanComponent, Map parameters, boolean throwException) + { + String eventName = null; + if(parameters != null) + { + eventName = (String)parameters.get(PARAM_EVENT_NAME); + } + if (this.nodeService.hasAspect(filePlanComponent, ASPECT_DISPOSITION_LIFECYCLE) == true) + { + // Get the next disposition action + DispositionAction da = this.dispositionService.getNextDispositionAction(filePlanComponent); + if (da != null) + { + // Get the disposition event + if(parameters != null) + { + EventCompletionDetails event = getEvent(da, eventName); + if (event != null) + { + return true; + } + else + { + if(throwException) + { + throw new AlfrescoRuntimeException(I18NUtil.getMessage(MSG_EVENT_NOT_DONE, eventName)); + } + } + } + else + { + return true; + } + } + } + return false; + } + + + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/UnfreezeAction.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/UnfreezeAction.java new file mode 100644 index 0000000000..9189184f75 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/UnfreezeAction.java @@ -0,0 +1,159 @@ +/* + * 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.module.org_alfresco_module_rm.action.impl; + +import java.io.Serializable; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.action.RMActionExecuterAbstractBase; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Unfreeze Action + * + * @author Roy Wetherall + */ +public class UnfreezeAction extends RMActionExecuterAbstractBase +{ + /** Logger */ + private static Log logger = LogFactory.getLog(UnfreezeAction.class); + + /** + * @see org.alfresco.repo.action.executer.ActionExecuterAbstractBase#executeImpl(org.alfresco.service.cmr.action.Action, + * org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + protected void executeImpl(Action action, NodeRef actionedUponNodeRef) + { + if (this.nodeService.hasAspect(actionedUponNodeRef, ASPECT_FROZEN) == true) + { + final boolean isFolder = this.recordsManagementService.isRecordFolder(actionedUponNodeRef); + + if (logger.isDebugEnabled()) + { + StringBuilder msg = new StringBuilder(); + msg.append("Unfreezing node ").append(actionedUponNodeRef); + if (isFolder) + { + msg.append(" (folder)"); + } + logger.debug(msg.toString()); + } + + // Remove freeze from node + removeFreeze(actionedUponNodeRef); + + // Remove freeze from records if a record folder + if (isFolder) + { + List records = this.recordsManagementService.getRecords(actionedUponNodeRef); + for (NodeRef record : records) + { + removeFreeze(record); + } + } + } + } + + /** + * Removes a freeze from a node + * + * @param nodeRef + * node reference + */ + private void removeFreeze(NodeRef nodeRef) + { + // Get all the holds and remove this node from them + List assocs = this.nodeService.getParentAssocs(nodeRef, ASSOC_FROZEN_RECORDS, RegexQNamePattern.MATCH_ALL); + + if (logger.isDebugEnabled()) + { + StringBuilder msg = new StringBuilder(); + msg.append("Removing freeze from node ").append(nodeRef) + .append("which has ").append(assocs.size()).append(" holds"); + logger.debug(msg.toString()); + } + + for (ChildAssociationRef assoc : assocs) + { + // Remove the frozen node as a child + NodeRef holdNodeRef = assoc.getParentRef(); + this.nodeService.removeChild(holdNodeRef, nodeRef); + + if (logger.isDebugEnabled()) + { + StringBuilder msg = new StringBuilder(); + msg.append("Removed frozen node from hold ").append(holdNodeRef); + logger.debug(msg.toString()); + } + + // Check to see if we should delete the hold + List holdAssocs = this.nodeService.getChildAssocs(holdNodeRef, ASSOC_FROZEN_RECORDS, RegexQNamePattern.MATCH_ALL); + if (holdAssocs.size() == 0) + { + if (logger.isDebugEnabled()) + { + StringBuilder msg = new StringBuilder(); + msg.append("Hold node ").append(holdNodeRef) + .append(" with name ").append(nodeService.getProperty(holdNodeRef, ContentModel.PROP_NAME)) + .append(" has no frozen nodes. Hence deleting it."); + logger.debug(msg.toString()); + } + + // Delete the hold object + this.nodeService.deleteNode(holdNodeRef); + } + } + + // Remove the aspect + this.nodeService.removeAspect(nodeRef, ASPECT_FROZEN); + + if (logger.isDebugEnabled()) + { + StringBuilder msg = new StringBuilder(); + msg.append("Removed frozen aspect from ").append(nodeRef); + logger.debug(msg.toString()); + } + } + + @Override + public Set getProtectedAspects() + { + HashSet qnames = new HashSet(); + qnames.add(ASPECT_FROZEN); + return qnames; + } + + @Override + protected boolean isExecutableImpl(NodeRef filePlanComponent, Map parameters, boolean throwException) + { + return this.nodeService.hasAspect(filePlanComponent, ASPECT_FROZEN); + } + +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/audit/AuditEvent.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/audit/AuditEvent.java new file mode 100644 index 0000000000..0cd942fd5c --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/audit/AuditEvent.java @@ -0,0 +1,68 @@ +/* + * 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.module.org_alfresco_module_rm.audit; + +import org.springframework.extensions.surf.util.I18NUtil; + +/** + * Class to represent an audit event + * + * @author Gavin Cornwell + */ +public class AuditEvent +{ + private final String name; + private final String label; + + /** + * Constructor + * + * @param name The audit event name + * @param label The audit event label (or I18N lookup id) + */ + public AuditEvent(String name, String label) + { + this.name = name; + + String lookup = I18NUtil.getMessage(label); + if (lookup != null) + { + label = lookup; + } + this.label = label; + } + + /** + * + * @return The audit event name + */ + public String getName() + { + return this.name; + } + + /** + * + * @return The audit event label + */ + public String getLabel() + { + return this.label; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/audit/AuthenticatedUserRolesDataExtractor.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/audit/AuthenticatedUserRolesDataExtractor.java new file mode 100644 index 0000000000..abd6fc71b4 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/audit/AuthenticatedUserRolesDataExtractor.java @@ -0,0 +1,111 @@ +/* + * 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.module.org_alfresco_module_rm.audit; + +import java.io.Serializable; +import java.util.Set; + +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService; +import org.alfresco.module.org_alfresco_module_rm.security.Role; +import org.alfresco.repo.audit.extractor.AbstractDataExtractor; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; + +/** + * An extractor that uses a node context to determine the currently-authenticated + * user's RM roles. This is not a data generator because it can only function in + * the context of a give node. + * + * @author Derek Hulley + * @since 3.2 + */ +public final class AuthenticatedUserRolesDataExtractor extends AbstractDataExtractor +{ + private NodeService nodeService; + private RecordsManagementService rmService; + private RecordsManagementSecurityService rmSecurityService; + + /** + * Used to check that the node in the context is a fileplan component + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Used to find the RM root + */ + public void setRmService(RecordsManagementService rmService) + { + this.rmService = rmService; + } + + /** + * Used to get roles + */ + public void setRmSecurityService(RecordsManagementSecurityService rmSecurityService) + { + this.rmSecurityService = rmSecurityService; + } + + /** + * @return Returns true if the data is a NodeRef and it represents + * a fileplan component + */ + public boolean isSupported(Serializable data) + { + if (data == null || !(data instanceof NodeRef)) + { + return false; + } + return nodeService.hasAspect((NodeRef)data, RecordsManagementModel.ASPECT_FILE_PLAN_COMPONENT); + } + + public Serializable extractData(Serializable value) throws Throwable + { + NodeRef nodeRef = (NodeRef) value; + String user = AuthenticationUtil.getFullyAuthenticatedUser(); + if (user == null) + { + // No-one is authenticated + return null; + } + + // Get the rm root + NodeRef rmRootNodeRef = rmService.getFilePlan(nodeRef); + + Set roles = rmSecurityService.getRolesByUser(rmRootNodeRef, user); + StringBuilder sb = new StringBuilder(100); + for (Role role : roles) + { + if (sb.length() > 0) + { + sb.append(", "); + } + sb.append(role.getDisplayLabel()); + } + + // Done + return sb.toString(); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/audit/FilePlanIdentifierDataExtractor.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/audit/FilePlanIdentifierDataExtractor.java new file mode 100644 index 0000000000..28faaa9905 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/audit/FilePlanIdentifierDataExtractor.java @@ -0,0 +1,70 @@ +/* + * 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.module.org_alfresco_module_rm.audit; + +import java.io.Serializable; + +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.repo.audit.extractor.AbstractDataExtractor; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; + +/** + * An extractor that gets a node's {@link RecordsManagementModel#PROP_IDENTIFIER identifier} property. + * This will only extract data if the node is a + * {@link RecordsManagementModel#ASPECT_RECORD_COMPONENT_ID Record component identifier}. + * + * @author Derek Hulley + * @since 3.2 + */ +public final class FilePlanIdentifierDataExtractor extends AbstractDataExtractor +{ + private NodeService nodeService; + + /** + * Used to check that the node in the context is a fileplan component + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * @return Returns true if the data is a NodeRef and it represents + * a fileplan component + */ + public boolean isSupported(Serializable data) + { + if (data == null || !(data instanceof NodeRef)) + { + return false; + } + return nodeService.hasAspect((NodeRef)data, RecordsManagementModel.ASPECT_RECORD_COMPONENT_ID); + } + + public Serializable extractData(Serializable value) throws Throwable + { + NodeRef nodeRef = (NodeRef) value; + + String identifier = (String) nodeService.getProperty(nodeRef, RecordsManagementModel.PROP_IDENTIFIER); + + // Done + return identifier; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/audit/FilePlanNamePathDataExtractor.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/audit/FilePlanNamePathDataExtractor.java new file mode 100644 index 0000000000..edd08cc022 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/audit/FilePlanNamePathDataExtractor.java @@ -0,0 +1,92 @@ +/* + * 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.module.org_alfresco_module_rm.audit; + +import java.io.Serializable; +import java.util.List; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.repo.audit.extractor.AbstractDataExtractor; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; + +/** + * An extractor that extracts the cm:name path from the RM root down to + * - and including - the node's own name. This will only extract data if the + * node is a {@link RecordsManagementModel#ASPECT_FILE_PLAN_COMPONENT fileplan component}. + * + * @see RecordsManagementService#getNodeRefPath(NodeRef) + * + * @author Derek Hulley + * @since 3.2 + */ +public final class FilePlanNamePathDataExtractor extends AbstractDataExtractor +{ + private NodeService nodeService; + private RecordsManagementService rmService; + + /** + * Used to check that the node in the context is a fileplan component + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Used to find the RM root + */ + public void setRmService(RecordsManagementService rmService) + { + this.rmService = rmService; + } + + /** + * @return Returns true if the data is a NodeRef and it represents + * a fileplan component + */ + public boolean isSupported(Serializable data) + { + if (data == null || !(data instanceof NodeRef)) + { + return false; + } + return nodeService.hasAspect((NodeRef)data, RecordsManagementModel.ASPECT_FILE_PLAN_COMPONENT); + } + + public Serializable extractData(Serializable value) throws Throwable + { + NodeRef nodeRef = (NodeRef) value; + + // Get path from the RM root + List nodeRefPath = rmService.getNodeRefPath(nodeRef); + + StringBuilder sb = new StringBuilder(128); + for (NodeRef pathNodeRef : nodeRefPath) + { + String name = (String)nodeService.getProperty(pathNodeRef, ContentModel.PROP_NAME); + sb.append("/").append(name); + } + + // Done + return sb.toString(); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/audit/FilePlanNodeRefPathDataExtractor.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/audit/FilePlanNodeRefPathDataExtractor.java new file mode 100644 index 0000000000..5248d6bb9a --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/audit/FilePlanNodeRefPathDataExtractor.java @@ -0,0 +1,84 @@ +/* + * 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.module.org_alfresco_module_rm.audit; + +import java.io.Serializable; +import java.util.List; + +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.repo.audit.extractor.AbstractDataExtractor; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; + +/** + * An extractor that extracts the NodeRef path from the RM root down to + * - and including - the node itself. This will only extract data if the + * node is a {@link RecordsManagementModel#ASPECT_FILE_PLAN_COMPONENT fileplan component}. + * + * @see RecordsManagementService#getNodeRefPath(NodeRef) + * + * @author Derek Hulley + * @since 3.2 + */ +public final class FilePlanNodeRefPathDataExtractor extends AbstractDataExtractor +{ + private NodeService nodeService; + private RecordsManagementService rmService; + + /** + * Used to check that the node in the context is a fileplan component + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Used to find the RM root + */ + public void setRmService(RecordsManagementService rmService) + { + this.rmService = rmService; + } + + /** + * @return Returns true if the data is a NodeRef and it represents + * a fileplan component + */ + public boolean isSupported(Serializable data) + { + if (data == null || !(data instanceof NodeRef)) + { + return false; + } + return nodeService.hasAspect((NodeRef)data, RecordsManagementModel.ASPECT_FILE_PLAN_COMPONENT); + } + + public Serializable extractData(Serializable value) throws Throwable + { + NodeRef nodeRef = (NodeRef) value; + + // Get path from the RM root + List nodeRefPath = rmService.getNodeRefPath(nodeRef); + + // Done + return (Serializable) nodeRefPath; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/audit/RecordsManagementAuditEntry.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/audit/RecordsManagementAuditEntry.java new file mode 100644 index 0000000000..b46f3a628e --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/audit/RecordsManagementAuditEntry.java @@ -0,0 +1,268 @@ +/* + * 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.module.org_alfresco_module_rm.audit; + +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.surf.util.ISO8601DateFormat; +import org.alfresco.util.Pair; +import org.alfresco.util.ParameterCheck; + +/** + * Class to represent a Records Management audit entry. + * + * @author Gavin Cornwell + */ +public final class RecordsManagementAuditEntry +{ + private final Date timestamp; + private final String userName; + private final String fullName; + private final String userRole; + private final NodeRef nodeRef; + private final String nodeName; + private final String nodeType; + private final String event; + private final String identifier; + private final String path; + private final Map beforeProperties; + private final Map afterProperties; + private Map> changedProperties; + + /** + * Default constructor + */ + public RecordsManagementAuditEntry(Date timestamp, + String userName, String fullName, String userRole, + NodeRef nodeRef, String nodeName, String nodeType, + String event, String identifier, String path, + Map beforeProperties, + Map afterProperties) + { + ParameterCheck.mandatory("timestamp", timestamp); + ParameterCheck.mandatory("userName", userName); + + this.timestamp = timestamp; + this.userName = userName; + this.userRole = userRole; + this.fullName = fullName; + this.nodeRef = nodeRef; + this.nodeName = nodeName; + this.nodeType = nodeType; + this.event = event; + this.identifier = identifier; + this.path = path; + this.beforeProperties = beforeProperties; + this.afterProperties = afterProperties; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("(") + .append("timestamp=").append(timestamp) + .append(", userName=").append(userName) + .append(", userRole=").append(userRole) + .append(", fullName=").append(fullName) + .append(", nodeRef=").append(nodeRef) + .append(", nodeName=").append(nodeName) + .append(", event=").append(event) + .append(", identifier=").append(identifier) + .append(", path=").append(path) + .append(", beforeProperties=").append(beforeProperties) + .append(", afterProperties=").append(afterProperties) + .append(", changedProperties=").append(changedProperties) + .append(")"); + return sb.toString(); + } + + /** + * + * @return The date of the audit entry + */ + public Date getTimestamp() + { + return this.timestamp; + } + + /** + * + * @return The date of the audit entry as an ISO8601 formatted String + */ + public String getTimestampString() + { + return ISO8601DateFormat.format(this.timestamp); + } + + /** + * + * @return The username of the user that caused the audit log entry to be created + */ + public String getUserName() + { + return this.userName; + } + + /** + * + * @return The full name of the user that caused the audit log entry to be created + */ + public String getFullName() + { + return this.fullName; + } + + /** + * + * @return The role of the user that caused the audit log entry to be created + */ + public String getUserRole() + { + return this.userRole; + } + + /** + * + * @return The NodeRef of the node the audit log entry is for + */ + public NodeRef getNodeRef() + { + return this.nodeRef; + } + + /** + * + * @return The name of the node the audit log entry is for + */ + public String getNodeName() + { + return this.nodeName; + } + + /** + * + * @return The type of the node the audit log entry is for + */ + public String getNodeType() + { + return this.nodeType; + } + + /** + * + * @return The human readable description of the reason for the audit log + * entry i.e. metadata updated, record declared + */ + public String getEvent() + { + return this.event; + } + + /** + * An identifier for the item being audited, for example for a record + * it will be the unique record identifier, for a user it would be the + * username etc. + * + * @return Ad identifier for the thing being audited + */ + public String getIdentifier() + { + return this.identifier; + } + + /** + * + * @return The path to the object being audited + */ + public String getPath() + { + return this.path; + } + + /** + * + * @return Map of properties before the audited action + */ + public Map getBeforeProperties() + { + return this.beforeProperties; + } + + /** + * + * @return Map of properties after the audited action + */ + public Map getAfterProperties() + { + return this.beforeProperties; + } + + /** + * + * @return Map of changed properties + */ + public Map> getChangedProperties() + { + if (this.changedProperties == null) + { + initChangedProperties(); + } + + return this.changedProperties; + } + + /** + * Initialises the map of changed values given the before and after properties + */ + private void initChangedProperties() + { + if (this.beforeProperties != null && this.afterProperties != null) + { + this.changedProperties = new HashMap>( + this.beforeProperties.size() + this.afterProperties.size()); + + // add all the properties present before the audited action + for (QName valuePropName : this.beforeProperties.keySet()) + { + Pair values = new Pair( + this.beforeProperties.get(valuePropName), + this.afterProperties.get(valuePropName)); + this.changedProperties.put(valuePropName, values); + } + + // add all the properties present after the audited action that + // have not already been added + for (QName valuePropName : this.afterProperties.keySet()) + { + if (!this.beforeProperties.containsKey(valuePropName)) + { + Pair values = new Pair(null, + this.afterProperties.get(valuePropName)); + this.changedProperties.put(valuePropName, values); + } + } + } + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/audit/RecordsManagementAuditQueryParameters.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/audit/RecordsManagementAuditQueryParameters.java new file mode 100644 index 0000000000..98c1618ec7 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/audit/RecordsManagementAuditQueryParameters.java @@ -0,0 +1,205 @@ +/* + * 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.module.org_alfresco_module_rm.audit; + +import java.util.Date; + +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + +/** + * Class to represent the parameters for a Records Management + * audit log query. + * + * @author Gavin Cornwell + */ +public final class RecordsManagementAuditQueryParameters +{ + private int maxEntries = -1; + private String user; + private NodeRef nodeRef; + private Date dateFrom; + private Date dateTo; + private String event; + private QName property; + + /** + * Default constructor. + */ + public RecordsManagementAuditQueryParameters() + { + } + + /** + * + * @return The username to filter by + */ + public String getUser() + { + return this.user; + } + + /** + * Restricts the retrieved audit trail to entries made by + * the provided user. + * + * @param user The username to filter by + */ + public void setUser(String user) + { + this.user = user; + } + + /** + * + * @return The maximum number of audit log entries to retrieve + */ + public int getMaxEntries() + { + return this.maxEntries; + } + + /** + * Restricts the retrieved audit trail to the last + * maxEntries entries. + * + * @param maxEntries Maximum number of entries + */ + public void setMaxEntries(int maxEntries) + { + this.maxEntries = maxEntries; + } + + /** + * + * @return The node to get entries for + */ + public NodeRef getNodeRef() + { + return this.nodeRef; + } + + /** + * Restricts the retrieved audit trail to only those entries + * created by the give node. + * + * @param nodeRef The node to get entries for + */ + public void setNodeRef(NodeRef nodeRef) + { + this.nodeRef = nodeRef; + } + + /** + * + * @return The date to retrieve entries from + */ + public Date getDateFrom() + { + return this.dateFrom; + } + + /** + * Restricts the retrieved audit trail to only those entries + * that occurred after the given date. + * + * @param dateFrom Date to retrieve entries after + */ + public void setDateFrom(Date dateFrom) + { + this.dateFrom = dateFrom; + } + + /** + * + * @return The date to retrive entries to + */ + public Date getDateTo() + { + return this.dateTo; + } + + /** + * Restricts the retrieved audit trail to only those entries + * that occurred before the given date. + * + * @param dateTo Date to retrieve entries before + */ + public void setDateTo(Date dateTo) + { + this.dateTo = dateTo; + } + + /** + * + * @return The event to retrive entries for + */ + public String getEvent() + { + return this.event; + } + + /** + * Restricts the retrieved audit trail to only those entries + * that match the given event string. + * + * @param event Event to retrieve entries for + */ + public void setEvent(String event) + { + this.event = event; + } + + /** + * + * @return The property to retrieve entries for + */ + public QName getProperty() + { + return this.property; + } + + /** + * Restricts the audit trail to only those entries that involve + * the given property. + * + * @param property The property to retrieve entries for + */ + public void setProperty(QName property) + { + this.property = property; + } + + /* + * @see java.lang.Object#toString() + */ + @Override + public String toString() + { + StringBuilder builder = new StringBuilder(super.toString()); + + builder.append(" (nodeRef='").append(nodeRef).append("', user='") + .append(user).append("', dateFrom='").append(dateFrom) + .append("', dateTo='").append(dateTo).append("', maxEntries='") + .append(maxEntries).append("', event='").append(event) + .append("', property='").append(property).append("')"); + + return builder.toString(); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/audit/RecordsManagementAuditService.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/audit/RecordsManagementAuditService.java new file mode 100644 index 0000000000..f493d4949c --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/audit/RecordsManagementAuditService.java @@ -0,0 +1,169 @@ +/* + * 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.module.org_alfresco_module_rm.audit; + +import java.io.File; +import java.io.Serializable; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Records management audit service. + * + * @author Gavin Cornwell + */ +public interface RecordsManagementAuditService +{ + public enum ReportFormat { HTML, JSON } + + public static final String RM_AUDIT_EVENT_UPDATE_RM_OBJECT = "Update RM Object"; + public static final String RM_AUDIT_EVENT_CREATE_RM_OBJECT = "Create RM Object"; + public static final String RM_AUDIT_EVENT_DELETE_RM_OBJECT = "Delete RM Object"; + public static final String RM_AUDIT_EVENT_LOGIN_SUCCESS = "Login.Success"; + public static final String RM_AUDIT_EVENT_LOGIN_FAILURE = "Login.Failure"; + + public static final String RM_AUDIT_APPLICATION_NAME = "RM"; + public static final String RM_AUDIT_PATH_ROOT = "/RM"; + public static final String RM_AUDIT_SNIPPET_EVENT = "/event"; + public static final String RM_AUDIT_SNIPPET_PERSON = "/person"; + public static final String RM_AUDIT_SNIPPET_NAME = "/name"; + public static final String RM_AUDIT_SNIPPET_NODE = "/node"; + public static final String RM_AUDIT_SNIPPET_CHANGES = "/changes"; + public static final String RM_AUDIT_SNIPPET_BEFORE = "/before"; + public static final String RM_AUDIT_SNIPPET_AFTER = "/after"; + + public static final String RM_AUDIT_DATA_PERSON_FULLNAME = "/RM/event/person/fullName"; + public static final String RM_AUDIT_DATA_PERSON_ROLES = "/RM/event/person/roles"; + public static final String RM_AUDIT_DATA_EVENT_NAME = "/RM/event/name/value"; + public static final String RM_AUDIT_DATA_NODE_NODEREF = "/RM/event/node/noderef"; + public static final String RM_AUDIT_DATA_NODE_NAME = "/RM/event/node/name"; + public static final String RM_AUDIT_DATA_NODE_TYPE = "/RM/event/node/type"; + public static final String RM_AUDIT_DATA_NODE_IDENTIFIER = "/RM/event/node/identifier"; + public static final String RM_AUDIT_DATA_NODE_NAMEPATH = "/RM/event/node/namePath"; + public static final String RM_AUDIT_DATA_NODE_CHANGES_BEFORE = "/RM/event/node/changes/before/value"; + public static final String RM_AUDIT_DATA_NODE_CHANGES_AFTER = "/RM/event/node/changes/after/value"; + + public static final String RM_AUDIT_DATA_LOGIN_USERNAME = "/RM/login/args/userName/value"; + public static final String RM_AUDIT_DATA_LOGIN_FULLNAME = "/RM/login/no-error/fullName"; + public static final String RM_AUDIT_DATA_LOGIN_ERROR = "/RM/login/error/value"; + + /** + * Starts RM auditing. + */ + void start(); + + /** + * Stops RM auditing. + */ + void stop(); + + /** + * Clears the RM audit trail. + */ + void clear(); + + /** + * Determines whether the RM audit log is currently enabled. + * + * @return true if RM auditing is active false otherwise + */ + boolean isEnabled(); + + /** + * Returns the date the RM audit was last started. + * + * @return Date the audit was last started + */ + Date getDateLastStarted(); + + /** + * Returns the date the RM audit was last stopped. + * + * @return Date the audit was last stopped + */ + Date getDateLastStopped(); + + /** + * An explicit call that RM actions can make to have the events logged. + * + * @param action the action that will be performed + * @param nodeRef the component being acted on + * @param parameters the action's parameters + */ + void auditRMAction(RecordsManagementAction action, NodeRef nodeRef, Map parameters); + + /** + * Retrieves a list of audit log entries using the provided parameters + * represented by the RecordsManagementAuditQueryParameters instance. + *

+ * The parameters are all optional so an empty RecordsManagementAuditQueryParameters + * object will result in ALL audit log entries for the RM system being + * returned. Setting the various parameters effectively filters the full + * audit trail. + * + * @param params Parameters to use to retrieve audit trail (never null) + * @param format The format the report should be produced in + * @return File containing JSON representation of audit trail + */ + File getAuditTrailFile(RecordsManagementAuditQueryParameters params, ReportFormat format); + + /** + * Retrieves a list of audit log entries using the provided parameters + * represented by the RecordsManagementAuditQueryParameters instance. + *

+ * The parameters are all optional so an empty RecordsManagementAuditQueryParameters + * object will result in ALL audit log entries for the RM system being + * returned. Setting the various parameters effectively filters the full + * audit trail. + * + * @param params Parameters to use to retrieve audit trail (never null) + * @return All entries for the audit trail + */ + List getAuditTrail(RecordsManagementAuditQueryParameters params); + + /** + * Retrieves a list of audit log entries using the provided parameters + * represented by the RecordsManagementAuditQueryParameters instance and + * then files the resulting log as an undeclared record in the record folder + * represented by the given NodeRef. + *

+ * The parameters are all optional so an empty RecordsManagementAuditQueryParameters + * object will result in ALL audit log entries for the RM system being + * returned. Setting the various parameters effectively filters the full + * audit trail. + * + * @param params Parameters to use to retrieve audit trail (never null) + * @param destination NodeRef representing a record folder in which to file the audit log + * @param format The format the report should be produced in + * @return NodeRef of the undeclared record filed + */ + NodeRef fileAuditTrailAsRecord(RecordsManagementAuditQueryParameters params, + NodeRef destination, ReportFormat format); + + /** + * Retrieves a list of audit events. + * + * @return List of audit events + */ + List getAuditEvents(); +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/audit/RecordsManagementAuditServiceImpl.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/audit/RecordsManagementAuditServiceImpl.java new file mode 100644 index 0000000000..70feec88db --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/audit/RecordsManagementAuditServiceImpl.java @@ -0,0 +1,1326 @@ +/* + * 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.module.org_alfresco_module_rm.audit; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Serializable; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction; +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.repo.audit.AuditComponent; +import org.alfresco.repo.audit.model.AuditApplication; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.node.NodeServicePolicies; +import org.alfresco.repo.node.NodeServicePolicies.BeforeDeleteNodePolicy; +import org.alfresco.repo.node.NodeServicePolicies.OnCreateNodePolicy; +import org.alfresco.repo.node.NodeServicePolicies.OnUpdatePropertiesPolicy; +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.transaction.AlfrescoTransactionSupport; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.TransactionListenerAdapter; +import org.alfresco.repo.transaction.TransactionalResourceHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.cmr.audit.AuditQueryParameters; +import org.alfresco.service.cmr.audit.AuditService; +import org.alfresco.service.cmr.audit.AuditService.AuditQueryCallback; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.dictionary.TypeDefinition; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +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; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.Pair; +import org.alfresco.util.PropertyCheck; +import org.alfresco.util.PropertyMap; +import org.alfresco.util.TempFileProvider; +import org.apache.commons.lang.StringEscapeUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.springframework.context.ApplicationEvent; +import org.springframework.extensions.surf.util.AbstractLifecycleBean; +import org.springframework.extensions.surf.util.I18NUtil; +import org.springframework.extensions.surf.util.ISO8601DateFormat; +import org.springframework.extensions.surf.util.ParameterCheck; + +/** + * Records Management Audit Service Implementation. + * + * @author Gavin Cornwell + * @since 3.2 + */ +public class RecordsManagementAuditServiceImpl + extends AbstractLifecycleBean + implements RecordsManagementAuditService, + NodeServicePolicies.OnCreateNodePolicy, + NodeServicePolicies.BeforeDeleteNodePolicy, + NodeServicePolicies.OnUpdatePropertiesPolicy +{ + /** I18N */ + private static final String MSG_UPDATED_METADATA = "rm.audit.updated-metadata"; + private static final String MSG_CREATED_OBJECT = "rm.audit.created-object"; + private static final String MSG_DELETE_OBJECT = "rm.audit.delte-object"; + private static final String MSG_LOGIN_SUCCEEDED = "rm.audit.login-succeeded"; + private static final String MSG_LOGIN_FAILED = "rm.audit.login-failed"; + private static final String MSG_FILED_RECORD = "rm.audit.filed-record"; + private static final String MSG_REVIEWED = "rm.audit.reviewed"; + private static final String MSG_CUT_OFF = "rm.audit.cut-off"; + private static final String MSG_REVERSED_CUT_OFF = "rm.audit.reversed-cut-off"; + private static final String MSG_DESTROYED_ITEM = "rm.audit.destroyed-item"; + private static final String MSG_OPENED_RECORD_FOLDER = "rm.audit.opened-record-folder"; + private static final String MSG_CLOSED_RECORD_FOLDER = "rm.audit.closed-record-folder"; + private static final String MSG_SETUP_RECORD_FOLDER = "rm.audit.setup-recorder-folder"; + private static final String MSG_DECLARED_RECORD = "rm.audit.declared-record"; + private static final String MSG_UNDECLARED_RECORD = "rm.audit.undeclared-record"; + private static final String MSG_FROZE_ITEM = "rm.audit.froze-item"; + private static final String MSG_RELINQUISED_HOLD = "rm.audit.relinquised-hold"; + private static final String MSG_UPDATED_HOLD_REASON = "rm.audit.updated-hold-reason"; + private static final String MSG_UPDATED_REVIEW_AS_OF_DATE = "rm.audit.updated-review-as-of-date"; + private static final String MSG_UPDATED_DISPOSITION_AS_OF_DATE = "rm.audit.updated-disposition-as-of-date"; + private static final String MSG_UPDATED_VITAL_RECORD_DEFINITION = "rm.audit.updated-vital-record-definition"; + private static final String MSG_UPDATED_DISPOSITOIN_ACTION_DEFINITION = "rm.audit.updated-disposition-action-definition"; + private static final String MSG_COMPELTED_EVENT = "rm.audit.completed-event"; + private static final String MSG_REVERSED_COMPLETE_EVENT = "rm.audit.revered-complete-event"; + private static final String MSG_TRANSFERRED_ITEM = "rm.audit.transferred-item"; + private static final String MSG_COMPLETED_TRANSFER = "rm.audit.completed-transfer"; + private static final String MSG_ACCESSION = "rm.audit.accession"; + private static final String MSG_COMPLETED_ACCESSION = "rm.audit.copmleted-accession"; + private static final String MSG_SCANNED_RECORD = "rm.audit.scanned-record"; + private static final String MSG_PDF_RECORD = "rm.audit.pdf-record"; + private static final String MSG_PHOTO_RECORD = "rm.audit.photo-record"; + private static final String MSG_WEB_RECORD = "rm.audit.web-record"; + private static final String MSG_TRAIL_FILE_FAIL = "rm.audit.trail-file-fail"; + private static final String MSG_AUDIT_REPORT = "rm.audit.audit-report"; + + /** Logger */ + private static Log logger = LogFactory.getLog(RecordsManagementAuditServiceImpl.class); + + private static final String KEY_RM_AUDIT_NODE_RECORDS = "RMAUditNodeRecords"; + + protected static final String AUDIT_TRAIL_FILE_PREFIX = "audit_"; + protected static final String AUDIT_TRAIL_JSON_FILE_SUFFIX = ".json"; + protected static final String AUDIT_TRAIL_HTML_FILE_SUFFIX = ".html"; + protected static final String FILE_ACTION = "file"; + + private PolicyComponent policyComponent; + private DictionaryService dictionaryService; + private TransactionService transactionService; + private NodeService nodeService; + private ContentService contentService; + private AuditComponent auditComponent; + private AuditService auditService; + private RecordsManagementService rmService; + private RecordsManagementActionService rmActionService; + + private boolean shutdown = false; + + private RMAuditTxnListener txnListener; + private Map auditEvents; + + public RecordsManagementAuditServiceImpl() + { + } + + /** + * Set the component used to bind to behaviour callbacks + */ + public void setPolicyComponent(PolicyComponent policyComponent) + { + this.policyComponent = policyComponent; + } + + /** + * Provides user-readable names for types + */ + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + /** + * Set the component used to start new transactions + */ + public void setTransactionService(TransactionService transactionService) + { + this.transactionService = transactionService; + } + + /** + * Sets the NodeService instance + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Sets the ContentService instance + */ + public void setContentService(ContentService contentService) + { + this.contentService = contentService; + } + + /** + * The component to create audit events + */ + public void setAuditComponent(AuditComponent auditComponent) + { + this.auditComponent = auditComponent; + } + + /** + * Sets the AuditService instance + */ + public void setAuditService(AuditService auditService) + { + this.auditService = auditService; + } + + /** + * Set the RecordsManagementService + */ + public void setRecordsManagementService(RecordsManagementService rmService) + { + this.rmService = rmService; + } + + /** + * Sets the RecordsManagementActionService instance + */ + public void setRecordsManagementActionService(RecordsManagementActionService rmActionService) + { + this.rmActionService = rmActionService; + } + + /** + * Checks that all necessary properties have been set. + */ + public void init() + { + PropertyCheck.mandatory(this, "policyComponent", policyComponent); + PropertyCheck.mandatory(this, "transactionService", transactionService); + PropertyCheck.mandatory(this, "nodeService", nodeService); + PropertyCheck.mandatory(this, "contentService", contentService); + PropertyCheck.mandatory(this, "auditComponent", auditComponent); + PropertyCheck.mandatory(this, "auditService", auditService); + PropertyCheck.mandatory(this, "rmService", rmService); + PropertyCheck.mandatory(this, "rmActionService", rmActionService); + PropertyCheck.mandatory(this, "dictionaryService", dictionaryService); + + // setup the audit events map + initAuditEvents(); + } + + protected void initAuditEvents() + { + // TODO: make this map configurable and localisable. + this.auditEvents = new HashMap(32); + + this.auditEvents.put(RM_AUDIT_EVENT_UPDATE_RM_OBJECT, + new AuditEvent(RM_AUDIT_EVENT_UPDATE_RM_OBJECT, MSG_UPDATED_METADATA)); + this.auditEvents.put(RM_AUDIT_EVENT_CREATE_RM_OBJECT, + new AuditEvent(RM_AUDIT_EVENT_CREATE_RM_OBJECT, MSG_CREATED_OBJECT)); + this.auditEvents.put(RM_AUDIT_EVENT_DELETE_RM_OBJECT, + new AuditEvent(RM_AUDIT_EVENT_DELETE_RM_OBJECT, MSG_DELETE_OBJECT)); + this.auditEvents.put(RM_AUDIT_EVENT_LOGIN_SUCCESS, + new AuditEvent(RM_AUDIT_EVENT_LOGIN_SUCCESS, MSG_LOGIN_SUCCEEDED)); + this.auditEvents.put(RM_AUDIT_EVENT_LOGIN_FAILURE, + new AuditEvent(RM_AUDIT_EVENT_LOGIN_FAILURE, MSG_LOGIN_FAILED)); + + this.auditEvents.put("file", + new AuditEvent("file", MSG_FILED_RECORD)); + this.auditEvents.put("reviewed", + new AuditEvent("reviewed", MSG_REVIEWED)); + this.auditEvents.put("cutoff", + new AuditEvent("cutoff", MSG_CUT_OFF)); + this.auditEvents.put("unCutoff", + new AuditEvent("unCutoff", MSG_REVERSED_CUT_OFF)); + this.auditEvents.put("destroy", + new AuditEvent("destroy", MSG_DESTROYED_ITEM)); + this.auditEvents.put("openRecordFolder", + new AuditEvent("openRecordFolder", MSG_OPENED_RECORD_FOLDER)); + this.auditEvents.put("closeRecordFolder", + new AuditEvent("closeRecordFolder", MSG_CLOSED_RECORD_FOLDER)); + this.auditEvents.put("setupRecordFolder", + new AuditEvent("setupRecordFolder", MSG_SETUP_RECORD_FOLDER)); + this.auditEvents.put("declareRecord", + new AuditEvent("declareRecord", MSG_DECLARED_RECORD)); + this.auditEvents.put("undeclareRecord", + new AuditEvent("undeclareRecord", MSG_UNDECLARED_RECORD)); + this.auditEvents.put("freeze", + new AuditEvent("freeze", MSG_FROZE_ITEM)); + this.auditEvents.put("relinquishHold", + new AuditEvent("relinquishHold", MSG_RELINQUISED_HOLD)); + this.auditEvents.put("editHoldReason", + new AuditEvent("editHoldReason", MSG_UPDATED_HOLD_REASON)); + this.auditEvents.put("editReviewAsOfDate", + new AuditEvent("editReviewAsOfDate", MSG_UPDATED_REVIEW_AS_OF_DATE)); + this.auditEvents.put("editDispositionActionAsOfDate", + new AuditEvent("editDispositionActionAsOfDate", MSG_UPDATED_DISPOSITION_AS_OF_DATE)); + this.auditEvents.put("broadcastVitalRecordDefinition", + new AuditEvent("broadcastVitalRecordDefinition", MSG_UPDATED_VITAL_RECORD_DEFINITION)); + this.auditEvents.put("broadcastDispositionActionDefinitionUpdate", + new AuditEvent("broadcastDispositionActionDefinitionUpdate", MSG_UPDATED_DISPOSITOIN_ACTION_DEFINITION)); + this.auditEvents.put("completeEvent", + new AuditEvent("completeEvent", MSG_COMPELTED_EVENT)); + this.auditEvents.put("undoEvent", + new AuditEvent("undoEvent", MSG_REVERSED_COMPLETE_EVENT)); + this.auditEvents.put("transfer", + new AuditEvent("transfer", MSG_TRANSFERRED_ITEM)); + this.auditEvents.put("transferComplete", + new AuditEvent("transferComplete", MSG_COMPLETED_TRANSFER)); + this.auditEvents.put("accession", + new AuditEvent("accession", MSG_ACCESSION)); + this.auditEvents.put("accessionComplete", + new AuditEvent("accessionComplete", MSG_COMPLETED_ACCESSION)); + this.auditEvents.put("applyScannedRecord", + new AuditEvent("applyScannedRecord", MSG_SCANNED_RECORD)); + this.auditEvents.put("applyPdfRecord", + new AuditEvent("applyPdfRecord", MSG_PDF_RECORD)); + this.auditEvents.put("applyDigitalPhotographRecord", + new AuditEvent("applyDigitalPhotographRecord", MSG_PHOTO_RECORD)); + this.auditEvents.put("applyWebRecord", + new AuditEvent("applyWebRecord", MSG_WEB_RECORD)); + } + + @Override + protected void onBootstrap(ApplicationEvent event) + { + shutdown = false; + txnListener = new RMAuditTxnListener(); + // Register to listen for property changes to rma:record types + policyComponent.bindClassBehaviour( + OnUpdatePropertiesPolicy.QNAME, + RecordsManagementModel.ASPECT_RECORD_COMPONENT_ID, + new JavaBehaviour(this, "onUpdateProperties")); + policyComponent.bindClassBehaviour( + OnCreateNodePolicy.QNAME, + RecordsManagementModel.ASPECT_RECORD_COMPONENT_ID, + new JavaBehaviour(this, "onCreateNode")); + policyComponent.bindClassBehaviour( + BeforeDeleteNodePolicy.QNAME, + RecordsManagementModel.ASPECT_RECORD_COMPONENT_ID, + new JavaBehaviour(this, "beforeDeleteNode")); + } + + @Override + protected void onShutdown(ApplicationEvent event) + { + shutdown = true; + } + + /** + * {@inheritDoc} + */ + public boolean isEnabled() + { + return auditService.isAuditEnabled( + RecordsManagementAuditService.RM_AUDIT_APPLICATION_NAME, + RecordsManagementAuditService.RM_AUDIT_PATH_ROOT); + } + + /** + * {@inheritDoc} + */ + public void start() + { + auditService.enableAudit( + RecordsManagementAuditService.RM_AUDIT_APPLICATION_NAME, + RecordsManagementAuditService.RM_AUDIT_PATH_ROOT); + if (logger.isInfoEnabled()) + logger.info("Started Records Management auditing"); + } + + /** + * {@inheritDoc} + */ + public void stop() + { + auditService.disableAudit( + RecordsManagementAuditService.RM_AUDIT_APPLICATION_NAME, + RecordsManagementAuditService.RM_AUDIT_PATH_ROOT); + if (logger.isInfoEnabled()) + logger.info("Stopped Records Management auditing"); + } + + /** + * {@inheritDoc} + */ + public void clear() + { + auditService.clearAudit(RecordsManagementAuditService.RM_AUDIT_APPLICATION_NAME, null, null); + if (logger.isInfoEnabled()) + logger.debug("Records Management audit log has been cleared"); + } + + /** + * {@inheritDoc} + */ + public Date getDateLastStarted() + { + // TODO: return proper date, for now it's today's date + return new Date(); + } + + /** + * {@inheritDoc} + */ + public Date getDateLastStopped() + { + // TODO: return proper date, for now it's today's date + return new Date(); + } + + /** + * A class to carry audit information through the transaction. + * + * @author Derek Hulley + * @since 3.2 + */ + private static class RMAuditNode + { + private String eventName; + private Map nodePropertiesBefore; + private Map nodePropertiesAfter; + + private RMAuditNode() + { + } + + public String getEventName() + { + return eventName; + } + + public void setEventName(String eventName) + { + this.eventName = eventName; + } + + public Map getNodePropertiesBefore() + { + return nodePropertiesBefore; + } + + public void setNodePropertiesBefore(Map nodePropertiesBefore) + { + this.nodePropertiesBefore = nodePropertiesBefore; + } + + public Map getNodePropertiesAfter() + { + return nodePropertiesAfter; + } + + public void setNodePropertiesAfter(Map nodePropertiesAfter) + { + this.nodePropertiesAfter = nodePropertiesAfter; + } + } + + public void onUpdateProperties(NodeRef nodeRef, Map before, Map after) + { + auditRMEvent(nodeRef, RM_AUDIT_EVENT_UPDATE_RM_OBJECT, before, after); + } + + public void beforeDeleteNode(NodeRef nodeRef) + { + auditRMEvent(nodeRef, RM_AUDIT_EVENT_DELETE_RM_OBJECT, null, null); + } + + public void onCreateNode(ChildAssociationRef childAssocRef) + { + auditRMEvent(childAssocRef.getChildRef(), RM_AUDIT_EVENT_CREATE_RM_OBJECT, null, null); + } + + /** + * {@inheritDoc} + * @since 3.2 + */ + public void auditRMAction( + RecordsManagementAction action, + NodeRef nodeRef, + Map parameters) + { + auditRMEvent(nodeRef, action.getName(), null, null); + } + + /** + * Audit an event for a node + * + * @param nodeRef the node to which the event applies + * @param eventName the name of the event + * @param nodePropertiesBefore properties before the event (optional) + * @param nodePropertiesAfter properties after the event (optional) + */ + private void auditRMEvent( + NodeRef nodeRef, + String eventName, + Map nodePropertiesBefore, + Map nodePropertiesAfter) + { + // If we are deleting nodes, then we need to audit NOW + if (eventName.equals(RecordsManagementAuditService.RM_AUDIT_EVENT_DELETE_RM_OBJECT)) + { + // Deleted nodes will not be available at the end of the transaction. The data needs to + // be extracted now and the audit entry needs to be created now. + Map auditMap = new HashMap(13); + auditMap.put( + AuditApplication.buildPath( + RecordsManagementAuditService.RM_AUDIT_SNIPPET_EVENT, + RecordsManagementAuditService.RM_AUDIT_SNIPPET_NAME), + eventName); + // Action node + auditMap.put( + AuditApplication.buildPath( + RecordsManagementAuditService.RM_AUDIT_SNIPPET_EVENT, + RecordsManagementAuditService.RM_AUDIT_SNIPPET_NODE), + nodeRef); + auditMap = auditComponent.recordAuditValues(RecordsManagementAuditService.RM_AUDIT_PATH_ROOT, auditMap); + if (logger.isDebugEnabled()) + { + logger.debug("RM Audit: Audited node deletion: \n" + auditMap); + } + } + else + { + // Create an event for auditing post-commit + Map auditedNodes = TransactionalResourceHelper.getMap(KEY_RM_AUDIT_NODE_RECORDS); + RMAuditNode auditedNode = auditedNodes.get(nodeRef); + if (auditedNode == null) + { + auditedNode = new RMAuditNode(); + auditedNodes.put(nodeRef, auditedNode); + // Bind the listener to the txn. We could do it anywhere in the method, this position ensures + // that we avoid some rebinding of the listener + AlfrescoTransactionSupport.bindListener(txnListener); + } + // Only update the eventName if it has not already been done + if (auditedNode.getEventName() == null) + { + auditedNode.setEventName(eventName); + } + // Set the properties before the start if they are not already available + if (auditedNode.getNodePropertiesBefore() == null) + { + auditedNode.setNodePropertiesBefore(nodePropertiesBefore); + } + // Set the after values if they are provided. + // Overwrite as we assume that these represent the latest state of the node. + if (nodePropertiesAfter != null) + { + auditedNode.setNodePropertiesAfter(nodePropertiesAfter); + } + // That is it. The values are queued for the end of the transaction. + } + } + + /** + * A stateless transaction listener for RM auditing. This component picks up the data of + * modified nodes and generates the audit information. + *

+ * This class is not static so that the instances will have access to the action's implementation. + * + * @author Derek Hulley + * @since 3.2 + */ + private class RMAuditTxnListener extends TransactionListenerAdapter + { + private final Log logger = LogFactory.getLog(RecordsManagementAuditServiceImpl.class); + + /* + * Equality and hashcode generation are left unimplemented; we expect to only have a single + * instance of this class per action. + */ + + /** + * Get the action parameters from the transaction and audit them. + */ + @Override + public void afterCommit() + { + final Map auditedNodes = TransactionalResourceHelper.getMap(KEY_RM_AUDIT_NODE_RECORDS); + + // Start a *new* read-write transaction to audit in + RetryingTransactionCallback auditCallback = new RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + auditInTxn(auditedNodes); + return null; + } + }; + transactionService.getRetryingTransactionHelper().doInTransaction(auditCallback, false, true); + } + + /** + * Do the actual auditing, assuming the presence of a viable transaction + * + * @param auditedNodes details of the nodes that were modified + */ + private void auditInTxn(Map auditedNodes) throws Throwable + { + // Go through all the audit information and audit it + boolean auditedSomething = false; // We rollback if nothing is audited + for (Map.Entry entry : auditedNodes.entrySet()) + { + NodeRef nodeRef = entry.getKey(); + + // If the node is gone, then do nothing + if (!nodeService.exists(nodeRef)) + { + continue; + } + + RMAuditNode auditedNode = entry.getValue(); + + Map auditMap = new HashMap(13); + // Action description + String eventName = auditedNode.getEventName(); + auditMap.put( + AuditApplication.buildPath( + RecordsManagementAuditService.RM_AUDIT_SNIPPET_EVENT, + RecordsManagementAuditService.RM_AUDIT_SNIPPET_NAME), + eventName); + // Action node + auditMap.put( + AuditApplication.buildPath( + RecordsManagementAuditService.RM_AUDIT_SNIPPET_EVENT, + RecordsManagementAuditService.RM_AUDIT_SNIPPET_NODE), + nodeRef); + // Property changes + Map propertiesBefore = auditedNode.getNodePropertiesBefore(); + Map propertiesAfter = auditedNode.getNodePropertiesAfter(); + Pair, Map> deltaPair = + PropertyMap.getBeforeAndAfterMapsForChanges(propertiesBefore, propertiesAfter); + auditMap.put( + AuditApplication.buildPath( + RecordsManagementAuditService.RM_AUDIT_SNIPPET_EVENT, + RecordsManagementAuditService.RM_AUDIT_SNIPPET_NODE, + RecordsManagementAuditService.RM_AUDIT_SNIPPET_CHANGES, + RecordsManagementAuditService.RM_AUDIT_SNIPPET_BEFORE), + (Serializable) deltaPair.getFirst()); + auditMap.put( + AuditApplication.buildPath( + RecordsManagementAuditService.RM_AUDIT_SNIPPET_EVENT, + RecordsManagementAuditService.RM_AUDIT_SNIPPET_NODE, + RecordsManagementAuditService.RM_AUDIT_SNIPPET_CHANGES, + RecordsManagementAuditService.RM_AUDIT_SNIPPET_AFTER), + (Serializable) deltaPair.getSecond()); + // Audit it + if (logger.isDebugEnabled()) + { + logger.debug("RM Audit: Auditing values: \n" + auditMap); + } + auditMap = auditComponent.recordAuditValues(RecordsManagementAuditService.RM_AUDIT_PATH_ROOT, auditMap); + if (auditMap.isEmpty()) + { + if (logger.isDebugEnabled()) + { + logger.debug("RM Audit: Nothing was audited."); + } + } + else + { + if (logger.isDebugEnabled()) + { + logger.debug("RM Audit: Audited values: \n" + auditMap); + } + // We must commit the transaction to get the values in + auditedSomething = true; + } + } + // Check if anything was audited + if (!auditedSomething) + { + // Nothing was audited, so do nothing + RetryingTransactionHelper.getActiveUserTransaction().setRollbackOnly(); + } + } + } + + /** + * {@inheritDoc} + */ + public File getAuditTrailFile(RecordsManagementAuditQueryParameters params, ReportFormat format) + { + ParameterCheck.mandatory("params", params); + + Writer fileWriter = null; + try + { + File auditTrailFile = TempFileProvider.createTempFile(AUDIT_TRAIL_FILE_PREFIX, + format == ReportFormat.HTML ? AUDIT_TRAIL_HTML_FILE_SUFFIX : AUDIT_TRAIL_JSON_FILE_SUFFIX); + fileWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(auditTrailFile),"UTF8")); + // Get the results, dumping to file + getAuditTrailImpl(params, null, fileWriter, format); + // Done + return auditTrailFile; + } + catch (Throwable e) + { + throw new AlfrescoRuntimeException(MSG_TRAIL_FILE_FAIL, e); + } + finally + { + // close the writer + if (fileWriter != null) + { + try { fileWriter.close(); } catch (IOException closeEx) {} + } + } + } + + /** + * {@inheritDoc} + */ + public List getAuditTrail(RecordsManagementAuditQueryParameters params) + { + ParameterCheck.mandatory("params", params); + + List entries = new ArrayList(50); + try + { + getAuditTrailImpl(params, entries, null, null); + // Done + return entries; + } + catch (Throwable e) + { + // Should be + throw new AlfrescoRuntimeException(MSG_TRAIL_FILE_FAIL, e); + } + } + + /** + * Get the audit trail, optionally dumping the results the the given writer dumping to a list. + * + * @param params the search parameters + * @param results the list to which individual results will be dumped + * @param writer Writer to write the audit trail + * @param reportFormat Format to write the audit trail in, ignored if writer is null + */ + private void getAuditTrailImpl( + RecordsManagementAuditQueryParameters params, + final List results, + final Writer writer, + final ReportFormat reportFormat) + throws IOException + { + if (logger.isDebugEnabled()) + logger.debug("Retrieving audit trail in '" + reportFormat + "' format using parameters: " + params); + + // define the callback + AuditQueryCallback callback = new AuditQueryCallback() + { + private boolean firstEntry = true; + + + public boolean valuesRequired() + { + return true; + } + + /** + * Just log the error, but continue + */ + public boolean handleAuditEntryError(Long entryId, String errorMsg, Throwable error) + { + logger.warn(errorMsg, error); + return true; + } + + @SuppressWarnings("unchecked") + public boolean handleAuditEntry( + Long entryId, + String applicationName, + String user, + long time, + Map values) + { + // Check for context shutdown + if (shutdown) + { + return false; + } + + Date timestamp = new Date(time); + String eventName = null; + String fullName = null; + String userRoles = null; + NodeRef nodeRef = null; + String nodeName = null; + String nodeType = null; + String nodeIdentifier = null; + String namePath = null; + Map beforeProperties = null; + Map afterProperties = null; + + if (values.containsKey(RecordsManagementAuditService.RM_AUDIT_DATA_EVENT_NAME)) + { + // This data is /RM/event/... + eventName = (String) values.get(RecordsManagementAuditService.RM_AUDIT_DATA_EVENT_NAME); + fullName = (String) values.get(RecordsManagementAuditService.RM_AUDIT_DATA_PERSON_FULLNAME); + userRoles = (String) values.get(RecordsManagementAuditService.RM_AUDIT_DATA_PERSON_ROLES); + nodeRef = (NodeRef) values.get(RecordsManagementAuditService.RM_AUDIT_DATA_NODE_NODEREF); + nodeName = (String) values.get(RecordsManagementAuditService.RM_AUDIT_DATA_NODE_NAME); + QName nodeTypeQname = (QName) values.get(RecordsManagementAuditService.RM_AUDIT_DATA_NODE_TYPE); + nodeIdentifier = (String) values.get(RecordsManagementAuditService.RM_AUDIT_DATA_NODE_IDENTIFIER); + namePath = (String) values.get(RecordsManagementAuditService.RM_AUDIT_DATA_NODE_NAMEPATH); + beforeProperties = (Map) values.get( + RecordsManagementAuditService.RM_AUDIT_DATA_NODE_CHANGES_BEFORE); + afterProperties = (Map) values.get( + RecordsManagementAuditService.RM_AUDIT_DATA_NODE_CHANGES_AFTER); + + // Convert some of the values to recognizable forms + nodeType = null; + if (nodeTypeQname != null) + { + TypeDefinition typeDef = dictionaryService.getType(nodeTypeQname); + nodeType = (typeDef != null) ? typeDef.getTitle() : null; + } + } + else if (values.containsKey(RecordsManagementAuditService.RM_AUDIT_DATA_LOGIN_USERNAME)) + { + user = (String) values.get(RecordsManagementAuditService.RM_AUDIT_DATA_LOGIN_USERNAME); + if (values.containsKey(RecordsManagementAuditService.RM_AUDIT_DATA_LOGIN_ERROR)) + { + eventName = RecordsManagementAuditService.RM_AUDIT_EVENT_LOGIN_FAILURE; + fullName = user; // The user didn't log in + } + else + { + eventName = RecordsManagementAuditService.RM_AUDIT_EVENT_LOGIN_SUCCESS; + fullName = (String) values.get(RecordsManagementAuditService.RM_AUDIT_DATA_LOGIN_FULLNAME); + } + } + else + { + // This is not recognisable data + logger.warn( + "Unable to process audit entry for RM. Unexpected data: \n" + + " Entry: " + entryId + "\n" + + " Data: " + values); + // Skip it + return true; + } + + // TODO: Refactor this to use the builder pattern + RecordsManagementAuditEntry entry = new RecordsManagementAuditEntry( + timestamp, + user, + fullName, + userRoles, // A concatenated string of roles + nodeRef, + nodeName, + nodeType, + eventName, + nodeIdentifier, + namePath, + beforeProperties, + afterProperties); + + // write out the entry to the file in requested format + writeEntryToFile(entry); + + if (results != null) + { + results.add(entry); + } + + if (logger.isDebugEnabled()) + { + logger.debug(" " + entry); + } + + // Keep going + return true; + } + + private void writeEntryToFile(RecordsManagementAuditEntry entry) + { + if (writer == null) + { + return; + } + try + { + if (!firstEntry) + { + if (reportFormat == ReportFormat.HTML) + { + writer.write("\n"); + } + else + { + writer.write(","); + } + } + else + { + firstEntry = false; + } + + // write the entry to the file + if (reportFormat == ReportFormat.JSON) + { + writer.write("\n\t\t"); + } + + writeAuditTrailEntry(writer, entry, reportFormat); + } + catch (IOException ioe) + { + throw new AlfrescoRuntimeException(MSG_TRAIL_FILE_FAIL, ioe); + } + } + }; + + String user = params.getUser(); + Long fromTime = (params.getDateFrom() == null ? null : new Long(params.getDateFrom().getTime())); + Long toTime = (params.getDateTo() == null ? null : new Long(params.getDateTo().getTime())); + NodeRef nodeRef = params.getNodeRef(); + String eventName = params.getEvent(); + QName propertyQName = params.getProperty(); + int maxEntries = params.getMaxEntries(); + boolean forward = maxEntries > 0 ? false : true; // Reverse order if the results are limited + + // start the audit trail report + writeAuditTrailHeader(writer, params, reportFormat); + + if (logger.isDebugEnabled()) + { + logger.debug("RM Audit: Issuing query: " + params); + } + + // Build audit query parameters + AuditQueryParameters auditQueryParams = new AuditQueryParameters(); + auditQueryParams.setForward(forward); + auditQueryParams.setApplicationName(RecordsManagementAuditService.RM_AUDIT_APPLICATION_NAME); + auditQueryParams.setUser(user); + auditQueryParams.setFromTime(fromTime); + auditQueryParams.setToTime(toTime); + if (nodeRef != null) + { + auditQueryParams.addSearchKey(RecordsManagementAuditService.RM_AUDIT_DATA_NODE_NODEREF, nodeRef); + } + // Get audit entries + auditService.auditQuery(callback, auditQueryParams, maxEntries); + + // finish off the audit trail report + writeAuditTrailFooter(writer, reportFormat); + } + + /** + * {@inheritDoc} + */ + public NodeRef fileAuditTrailAsRecord(RecordsManagementAuditQueryParameters params, + NodeRef destination, ReportFormat format) + { + ParameterCheck.mandatory("params", params); + ParameterCheck.mandatory("destination", destination); + + // NOTE: the underlying RM services will check all the remaining pre-conditions + + NodeRef record = null; + + // get the audit trail for the provided parameters + File auditTrail = this.getAuditTrailFile(params, format); + + if (logger.isDebugEnabled()) + { + logger.debug("Filing audit trail in file " + auditTrail.getAbsolutePath() + + " as a record in record folder: " + destination); + } + + try + { + Map properties = new HashMap(1); + properties.put(ContentModel.PROP_NAME, auditTrail.getName()); + + // file the audit log as an undeclared record + record = this.nodeService.createNode(destination, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, + QName.createValidLocalName(auditTrail.getName())), + ContentModel.TYPE_CONTENT, properties).getChildRef(); + + // Set the content + ContentWriter writer = this.contentService.getWriter(record, ContentModel.PROP_CONTENT, true); + writer.setMimetype(format == ReportFormat.HTML ? MimetypeMap.MIMETYPE_HTML : MimetypeMap.MIMETYPE_JSON); + writer.setEncoding("UTF-8"); + writer.putContent(auditTrail); + + // file the node as a record + this.rmActionService.executeRecordsManagementAction(record, FILE_ACTION); + } + finally + { + if (logger.isDebugEnabled()) + { + logger.debug("Audit trail report saved to temporary file: " + auditTrail.getAbsolutePath()); + } + else + { + auditTrail.delete(); + } + } + + return record; + } + + /** + * {@inheritDoc} + */ + public List getAuditEvents() + { + List listAuditEvents = new ArrayList(this.auditEvents.size()); + listAuditEvents.addAll(this.auditEvents.values()); + return listAuditEvents; + } + + /** + * Writes the start of the audit trail stream to the given writer + * + * @param writer The writer to write to + * @params params The parameters being used + * @param reportFormat The format to write the header in + * @throws IOException + */ + private void writeAuditTrailHeader(Writer writer, + RecordsManagementAuditQueryParameters params, + ReportFormat reportFormat) throws IOException + { + if (writer == null) + { + return; + } + + if (reportFormat == ReportFormat.HTML) + { + // write header as HTML + writer.write("\n"); + writer.write("\n\n"); + writer.write(""); + writer.write(I18NUtil.getMessage(MSG_AUDIT_REPORT)); + writer.write("\n"); + writer.write("\n"); + writer.write("\n

"); + writer.write(I18NUtil.getMessage(MSG_AUDIT_REPORT)); + writer.write("

\n"); + writer.write("
\n"); + + writer.write("From:"); + writer.write(""); + Date from = params.getDateFrom(); + writer.write(from == null ? "<Not Set>" : StringEscapeUtils.escapeHtml(from.toString())); + writer.write(""); + + writer.write("To:"); + writer.write(""); + Date to = params.getDateTo(); + writer.write(to == null ? "<Not Set>" : StringEscapeUtils.escapeHtml(to.toString())); + writer.write(""); + + writer.write("Property:"); + writer.write(""); + QName prop = params.getProperty(); + writer.write(prop == null ? "All" : StringEscapeUtils.escapeHtml(getPropertyLabel(prop))); + writer.write(""); + + writer.write("User:"); + writer.write(""); + writer.write(params.getUser() == null ? "All" : StringEscapeUtils.escapeHtml(params.getUser())); + writer.write(""); + + writer.write("Event:"); + writer.write(""); + writer.write(params.getEvent() == null ? "All" : StringEscapeUtils.escapeHtml(getAuditEventLabel(params.getEvent()))); + writer.write("\n"); + + writer.write("
\n"); + } + else + { + // write header as JSON + writer.write("{\n\t\"data\":\n\t{"); + writer.write("\n\t\t\"started\": \""); + writer.write(ISO8601DateFormat.format(getDateLastStarted())); + writer.write("\",\n\t\t\"stopped\": \""); + writer.write(ISO8601DateFormat.format(getDateLastStopped())); + writer.write("\",\n\t\t\"enabled\": "); + writer.write(Boolean.toString(isEnabled())); + writer.write(",\n\t\t\"entries\":["); + } + } + + /** + * Writes an audit trail entry to the given writer + * + * @param writer The writer to write to + * @param entry The entry to write + * @param reportFormat The format to write the header in + * @throws IOException + */ + private void writeAuditTrailEntry(Writer writer, RecordsManagementAuditEntry entry, + ReportFormat reportFormat) throws IOException + { + if (writer == null) + { + return; + } + + if (reportFormat == ReportFormat.HTML) + { + writer.write("
\n"); + writer.write("
"); + writer.write("Timestamp:"); + writer.write(""); + writer.write(StringEscapeUtils.escapeHtml(entry.getTimestamp().toString())); + writer.write(""); + writer.write("User:"); + writer.write(""); + writer.write(entry.getFullName() != null ? + StringEscapeUtils.escapeHtml(entry.getFullName()) : + StringEscapeUtils.escapeHtml(entry.getUserName())); + writer.write(""); + if (entry.getUserRole() != null && entry.getUserRole().length() > 0) + { + writer.write("Role:"); + writer.write(""); + writer.write(StringEscapeUtils.escapeHtml(entry.getUserRole())); + writer.write(""); + } + if (entry.getEvent() != null && entry.getEvent().length() > 0) + { + writer.write("Event:"); + writer.write(""); + writer.write(StringEscapeUtils.escapeHtml(getAuditEventLabel(entry.getEvent()))); + writer.write("\n"); + } + writer.write("
\n"); + writer.write("
"); + if (entry.getIdentifier() != null && entry.getIdentifier().length() > 0) + { + writer.write("Identifier:"); + writer.write(""); + writer.write(StringEscapeUtils.escapeHtml(entry.getIdentifier())); + writer.write(""); + } + if (entry.getNodeType() != null && entry.getNodeType().length() > 0) + { + writer.write("Type:"); + writer.write(""); + writer.write(StringEscapeUtils.escapeHtml(entry.getNodeType())); + writer.write(""); + } + if (entry.getPath() != null && entry.getPath().length() > 0) + { + // we need to strip off the first part of the path + String path = entry.getPath(); + String displayPath = path; + int idx = path.indexOf("/", 1); + if (idx != -1) + { + displayPath = "/File Plan" + path.substring(idx); + } + + writer.write("Location:"); + writer.write(""); + writer.write(StringEscapeUtils.escapeHtml(displayPath)); + writer.write(""); + } + writer.write("
\n"); + + if (entry.getChangedProperties() != null) + { + writer.write(""); + writer.write(""); + + // create an entry for each property that changed + for (QName valueName : entry.getChangedProperties().keySet()) + { + Pair values = entry.getChangedProperties().get(valueName); + writer.write(""); + } + + writer.write("
PropertyPrevious ValueNew Value
"); + writer.write(getPropertyLabel(valueName)); + writer.write(""); + Serializable oldValue = values.getFirst(); + writer.write(oldValue == null ? "<none>" : StringEscapeUtils.escapeHtml(oldValue.toString())); + writer.write(""); + Serializable newValue = values.getSecond(); + writer.write(newValue == null ? "<none>" : StringEscapeUtils.escapeHtml(newValue.toString())); + writer.write("
\n"); + } + + writer.write("
"); + } + else + { + try + { + JSONObject json = new JSONObject(); + + json.put("timestamp", entry.getTimestampString()); + json.put("userName", entry.getUserName()); + json.put("userRole", entry.getUserRole() == null ? "": entry.getUserRole()); + json.put("fullName", entry.getFullName() == null ? "": entry.getFullName()); + json.put("nodeRef", entry.getNodeRef() == null ? "": entry.getNodeRef()); + json.put("nodeName", entry.getNodeName() == null ? "": entry.getNodeName()); + json.put("nodeType", entry.getNodeType() == null ? "": entry.getNodeType()); + json.put("event", entry.getEvent() == null ? "": getAuditEventLabel(entry.getEvent())); + json.put("identifier", entry.getIdentifier() == null ? "": entry.getIdentifier()); + json.put("path", entry.getPath() == null ? "": entry.getPath()); + + JSONArray changedValues = new JSONArray(); + + if (entry.getChangedProperties() != null) + { + // create an entry for each property that changed + for (QName valueName : entry.getChangedProperties().keySet()) + { + Pair values = entry.getChangedProperties().get(valueName); + + JSONObject changedValue = new JSONObject(); + changedValue.put("name", getPropertyLabel(valueName)); + changedValue.put("previous", values.getFirst() == null ? "" : values.getFirst().toString()); + changedValue.put("new", values.getSecond() == null ? "" : values.getSecond().toString()); + + changedValues.put(changedValue); + } + } + + json.put("changedValues", changedValues); + + writer.write(json.toString()); + } + catch (JSONException je) + { + writer.write("{}"); + } + } + } + + /** + * Writes the end of the audit trail stream to the given writer + * + * @param writer The writer to write to + * @param reportFormat The format to write the footer in + * @throws IOException + */ + private void writeAuditTrailFooter(Writer writer, ReportFormat reportFormat) throws IOException + { + if (writer == null) + { + return; + } + + if (reportFormat == ReportFormat.HTML) + { + // write footer as HTML + writer.write("\n"); + } + else + { + // write footer as JSON + writer.write("\n\t\t]\n\t}\n}"); + } + } + + /** + * Returns the display label for a property QName + * + * @param property The property to get label for + * @param ddService DictionaryService instance + * @param namespaceService NamespaceService instance + * @return The label + */ + private String getPropertyLabel(QName property) + { + String label = null; + + PropertyDefinition propDef = this.dictionaryService.getProperty(property); + if (propDef != null) + { + label = propDef.getTitle(); + } + + if (label == null) + { + label = property.getLocalName(); + } + + return label; + } + + /** + * Returns the display label for the given audit event key + * + * @param eventKey The audit event key + * @return The display label or null if the key does not exist + */ + private String getAuditEventLabel(String eventKey) + { + String label = eventKey; + + AuditEvent event = this.auditEvents.get(eventKey); + if (event != null) + { + label = event.getLabel(); + } + + return label; + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/Capability.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/Capability.java new file mode 100644 index 0000000000..33e1d675d0 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/Capability.java @@ -0,0 +1,88 @@ +/* + * 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.module.org_alfresco_module_rm.capability; + +import java.util.List; + +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.AccessStatus; + +/** + * Capability Interface. + * + * @author andyh + */ +public interface Capability +{ + /** + * Does this capability apply to this nodeRef? + * @param nodeRef + * @return + */ + AccessStatus hasPermission(NodeRef nodeRef); + + /** + * + * @param nodeRef + * @return + */ + int hasPermissionRaw(NodeRef nodeRef); + + /** + * Evaluates the capability. + * + * @param nodeRef + * @return + */ + int evaluate(NodeRef nodeRef); + + /** + * + * @param source + * @param target + * @return + */ + int evaluate(NodeRef source, NodeRef target); + + /** + * Indicates whether this is a group capability or not + * + * @return + */ + boolean isGroupCapability(); + + /** + * Get the name of the capability + * @return + */ + String getName(); + + /** + * Get the name of optional actions tied to this capability + * @return + */ + List getActionNames(); + + /** + * + * @return + */ + List getActions(); +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/CapabilityService.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/CapabilityService.java new file mode 100644 index 0000000000..6e5a32a88e --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/CapabilityService.java @@ -0,0 +1,67 @@ +/* + * 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.module.org_alfresco_module_rm.capability; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.AccessStatus; + +/** + * @author Roy Wetherall + * @since 2.0 + */ +public interface CapabilityService +{ + /** + * + * @param capability + */ + void registerCapability(Capability capability); + + /** + * + * @param name + * @return + */ + Capability getCapability(String name); + + /** + * + * @return + */ + Set getCapabilities(); + + /** + * + * @param nodeRef + * @return + */ + Map getCapabilitiesAccessState(NodeRef nodeRef); + + /** + * + * @param nodeRef + * @param capabilityNames + * @return + */ + Map getCapabilitiesAccessState(NodeRef nodeRef, List capabilityNames); +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/CapabilityServiceImpl.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/CapabilityServiceImpl.java new file mode 100644 index 0000000000..4eabad974f --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/CapabilityServiceImpl.java @@ -0,0 +1,104 @@ +/* + * 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.module.org_alfresco_module_rm.capability; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.AccessStatus; + +/** + * @author Roy Wetherall + * @since 2.0 + */ +public class CapabilityServiceImpl implements CapabilityService +{ + /** Capabilities */ + private Map capabilities = new HashMap(57); + + /** + * @see org.alfresco.module.org_alfresco_module_rm.capability.CapabilityService#getCapability(java.lang.String) + */ + @Override + public Capability getCapability(String name) + { + return capabilities.get(name); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.capability.CapabilityService#registerCapability(org.alfresco.module.org_alfresco_module_rm.capability.Capability) + */ + @Override + public void registerCapability(Capability capability) + { + capabilities.put(capability.getName(), capability); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.capability.CapabilityService#getCapabilities() + */ + @Override + public Set getCapabilities() + { + return new HashSet(capabilities.values()); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.capability.CapabilityService#getCapabilitiesAccessState(org.alfresco.service.cmr.repository.NodeRef) + */ + public Map getCapabilitiesAccessState(NodeRef nodeRef) + { + HashMap answer = new HashMap(); + for (Capability capability : capabilities.values()) + { + AccessStatus status = capability.hasPermission(nodeRef); + if (answer.put(capability, status) != null) + { + throw new IllegalStateException(); + } + } + return answer; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.capability.CapabilityService#getCapabilitiesAccessState(org.alfresco.service.cmr.repository.NodeRef, java.util.List) + */ + public Map getCapabilitiesAccessState(NodeRef nodeRef, List capabilityNames) + { + HashMap answer = new HashMap(); + for (String capabilityName : capabilityNames) + { + Capability capability = capabilities.get(capabilityName); + if (capability != null) + { + AccessStatus status = capability.hasPermission(nodeRef); + if (answer.put(capability, status) != null) + { + throw new IllegalStateException(); + } + } + } + return answer; + } + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/RMActionProxyFactoryBean.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/RMActionProxyFactoryBean.java new file mode 100644 index 0000000000..5470a46f85 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/RMActionProxyFactoryBean.java @@ -0,0 +1,69 @@ +/* + * 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.module.org_alfresco_module_rm.capability; + +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction; +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService; +import org.alfresco.repo.action.RuntimeActionService; +import org.alfresco.repo.action.executer.ActionExecuter; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.springframework.aop.framework.ProxyFactoryBean; + +public class RMActionProxyFactoryBean extends ProxyFactoryBean +{ + private static final long serialVersionUID = 539749542853266449L; + + protected RuntimeActionService runtimeActionService; + + private RecordsManagementActionService recordsManagementActionService; + + /** + * Set action service + * + * @param actionService + */ + public void setRuntimeActionService(RuntimeActionService runtimeActionService) + { + this.runtimeActionService = runtimeActionService; + } + + /** + * Set records management service + * + * @param recordsManagementActionService + */ + public void setRecordsManagementActionService(RecordsManagementActionService recordsManagementActionService) + { + this.recordsManagementActionService = recordsManagementActionService; + } + + public void registerAction() + { + AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public Void doWork() throws Exception + { + runtimeActionService.registerActionExecuter((ActionExecuter) getObject()); + recordsManagementActionService.register((RecordsManagementAction) getObject()); + return null; + } + }, AuthenticationUtil.getSystemUserName()); + + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/RMAfterInvocationProvider.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/RMAfterInvocationProvider.java new file mode 100644 index 0000000000..672ec6e8a7 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/RMAfterInvocationProvider.java @@ -0,0 +1,977 @@ +/* + * 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.module.org_alfresco_module_rm.capability; + +import java.util.ArrayList; +import java.util.BitSet; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.StringTokenizer; + +import net.sf.acegisecurity.AccessDeniedException; +import net.sf.acegisecurity.Authentication; +import net.sf.acegisecurity.ConfigAttribute; +import net.sf.acegisecurity.ConfigAttributeDefinition; +import net.sf.acegisecurity.afterinvocation.AfterInvocationProvider; +import net.sf.acegisecurity.vote.AccessDecisionVoter; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.repo.search.SimpleResultSetMetaData; +import org.alfresco.repo.search.impl.lucene.PagingLuceneResultSet; +import org.alfresco.repo.search.impl.querymodel.QueryEngineResults; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.permissions.PermissionCheckCollection; +import org.alfresco.repo.security.permissions.PermissionCheckValue; +import org.alfresco.repo.security.permissions.PermissionCheckedValue; +import org.alfresco.repo.security.permissions.PermissionCheckedCollection.PermissionCheckedCollectionMixin; +import org.alfresco.repo.security.permissions.impl.acegi.ACLEntryVoterException; +import org.alfresco.repo.security.permissions.impl.acegi.FilteringResultSet; +import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; +import org.alfresco.service.cmr.search.LimitBy; +import org.alfresco.service.cmr.search.PermissionEvaluationMode; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.security.AccessStatus; +import org.aopalliance.intercept.MethodInvocation; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.InitializingBean; + +public class RMAfterInvocationProvider extends RMSecurityCommon + implements AfterInvocationProvider, InitializingBean +{ + private static Log logger = LogFactory.getLog(RMAfterInvocationProvider.class); + + private static final String AFTER_RM = "AFTER_RM"; + + private int maxPermissionChecks; + + private long maxPermissionCheckTimeMillis; + + public boolean supports(ConfigAttribute attribute) + { + if ((attribute.getAttribute() != null) && (attribute.getAttribute().startsWith(AFTER_RM))) + { + return true; + } + else + { + return false; + } + } + + @SuppressWarnings("unchecked") + public boolean supports(Class clazz) + { + return (MethodInvocation.class.isAssignableFrom(clazz)); + } + + public void afterPropertiesSet() throws Exception + { + } + + /** + * Default constructor + */ + public RMAfterInvocationProvider() + { + super(); + maxPermissionChecks = Integer.MAX_VALUE; + maxPermissionCheckTimeMillis = Long.MAX_VALUE; + } + + /** + * Set the max number of permission checks + * + * @param maxPermissionChecks + */ + public void setMaxPermissionChecks(int maxPermissionChecks) + { + this.maxPermissionChecks = maxPermissionChecks; + } + + /** + * Set the max time for permission checks + * + * @param maxPermissionCheckTimeMillis + */ + public void setMaxPermissionCheckTimeMillis(long maxPermissionCheckTimeMillis) + { + this.maxPermissionCheckTimeMillis = maxPermissionCheckTimeMillis; + } + + @SuppressWarnings("unchecked") + public Object decide(Authentication authentication, Object object, ConfigAttributeDefinition config, Object returnedObject) throws AccessDeniedException + { + if (logger.isDebugEnabled()) + { + MethodInvocation mi = (MethodInvocation) object; + if (mi == null) + { + logger.debug("Method is null."); + } + else + { + logger.debug("Method: " + mi.getMethod().toString()); + } + } + try + { + if (AuthenticationUtil.isRunAsUserTheSystemUser()) + { + if (logger.isDebugEnabled()) + { + logger.debug("Allowing system user access"); + } + return returnedObject; + } + else if (returnedObject == null) + { + if (logger.isDebugEnabled()) + { + logger.debug("Allowing null object access"); + } + return null; + } + else if (PermissionCheckedValue.class.isAssignableFrom(returnedObject.getClass())) + { + return decide(authentication, object, config, (PermissionCheckedValue) returnedObject); + } + else if (PermissionCheckValue.class.isAssignableFrom(returnedObject.getClass())) + { + return decide(authentication, object, config, (PermissionCheckValue) returnedObject); + } + else if (StoreRef.class.isAssignableFrom(returnedObject.getClass())) + { + return decide(authentication, object, config, nodeService.getRootNode((StoreRef) returnedObject)).getStoreRef(); + } + else if (NodeRef.class.isAssignableFrom(returnedObject.getClass())) + { + return decide(authentication, object, config, (NodeRef) returnedObject); + } + else if (ChildAssociationRef.class.isAssignableFrom(returnedObject.getClass())) + { + return decide(authentication, object, config, (ChildAssociationRef) returnedObject); + } + else if (AssociationRef.class.isAssignableFrom(returnedObject.getClass())) + { + return decide(authentication, object, config, (AssociationRef) returnedObject); + } + else if (ResultSet.class.isAssignableFrom(returnedObject.getClass())) + { + return decide(authentication, object, config, (ResultSet) returnedObject); + } + else if (PagingLuceneResultSet.class.isAssignableFrom(returnedObject.getClass())) + { + return decide(authentication, object, config, (PagingLuceneResultSet) returnedObject); + } + else if (QueryEngineResults.class.isAssignableFrom(returnedObject.getClass())) + { + return decide(authentication, object, config, (QueryEngineResults) returnedObject); + } + else if (Collection.class.isAssignableFrom(returnedObject.getClass())) + { + return decide(authentication, object, config, (Collection) returnedObject); + } + else if (returnedObject.getClass().isArray()) + { + return decide(authentication, object, config, (Object[]) returnedObject); + } + else if (Map.class.isAssignableFrom(returnedObject.getClass())) + { + return decide(authentication, object, config, (Map) returnedObject); + } + else + { + if (logger.isDebugEnabled()) + { + logger.debug("Uncontrolled object - access allowed for " + object.getClass().getName()); + } + return returnedObject; + } + } + catch (AccessDeniedException ade) + { + if (logger.isDebugEnabled()) + { + logger.debug("Access denied"); + ade.printStackTrace(); + } + throw ade; + } + catch (RuntimeException re) + { + if (logger.isDebugEnabled()) + { + logger.debug("Access denied by runtime exception"); + re.printStackTrace(); + } + throw re; + } + + } + + private PermissionCheckedValue decide(Authentication authentication, Object object, ConfigAttributeDefinition config, PermissionCheckedValue returnedObject) throws AccessDeniedException + { + // This passes as it has already been filtered + // TODO: Get the filter that was applied and double-check + return returnedObject; + } + + private PermissionCheckValue decide(Authentication authentication, Object object, ConfigAttributeDefinition config, PermissionCheckValue returnedObject) throws AccessDeniedException + { + // Get the wrapped value + NodeRef nodeRef = returnedObject.getNodeRef(); + decide(authentication, object, config, nodeRef); + // This passes + return returnedObject; + } + + private NodeRef decide(Authentication authentication, Object object, ConfigAttributeDefinition config, NodeRef returnedObject) throws AccessDeniedException + + { + if (returnedObject == null) + { + return null; + } + + if (isUnfiltered(returnedObject)) + { + return returnedObject; + } + + List supportedDefinitions = extractSupportedDefinitions(config); + if (supportedDefinitions.size() == 0) + { + return returnedObject; + } + + int parentResult = checkRead(nodeService.getPrimaryParent(returnedObject).getParentRef()); + int childResult = checkRead(returnedObject); + checkSupportedDefinitions(supportedDefinitions, parentResult, childResult); + + return returnedObject; + } + + private void checkSupportedDefinitions(List supportedDefinitions, int parentResult, int childResult) + { + for (ConfigAttributeDefintion cad : supportedDefinitions) + { + if (cad.parent == true && parentResult == AccessDecisionVoter.ACCESS_DENIED) + { + throw new AccessDeniedException("Access Denied"); + } + else if (cad.parent == false && childResult == AccessDecisionVoter.ACCESS_DENIED) + { + throw new AccessDeniedException("Access Denied"); + } + } + } + + private boolean isUnfiltered(NodeRef nodeRef) + { + return !nodeService.hasAspect(nodeRef, RecordsManagementModel.ASPECT_FILE_PLAN_COMPONENT); + + } + + @SuppressWarnings({"unchecked" }) + private List extractSupportedDefinitions(ConfigAttributeDefinition config) + { + List definitions = new ArrayList(); + Iterator iter = config.getConfigAttributes(); + + while (iter.hasNext()) + { + ConfigAttribute attr = (ConfigAttribute) iter.next(); + + if (this.supports(attr)) + { + definitions.add(new ConfigAttributeDefintion(attr)); + } + + } + return definitions; + } + + private ChildAssociationRef decide(Authentication authentication, Object object, ConfigAttributeDefinition config, ChildAssociationRef returnedObject) + throws AccessDeniedException + + { + if (returnedObject == null) + { + return null; + } + + List supportedDefinitions = extractSupportedDefinitions(config); + + if (supportedDefinitions.size() == 0) + { + return returnedObject; + } + + int parentReadCheck = checkRead(returnedObject.getParentRef()); + int childReadCheck = checkRead(returnedObject.getChildRef()); + + for (ConfigAttributeDefintion cad : supportedDefinitions) + { + NodeRef testNodeRef = null; + + if (cad.typeString.equals(cad.parent) == true) + { + testNodeRef = returnedObject.getParentRef(); + } + else + { + testNodeRef = returnedObject.getChildRef(); + } + + // Enforce Read Policy + + if (isUnfiltered(testNodeRef)) + { + continue; + } + + if (cad.typeString.equals(cad.parent) == true && parentReadCheck != AccessDecisionVoter.ACCESS_GRANTED) + { + throw new AccessDeniedException("Access Denied"); + } + else if (childReadCheck != AccessDecisionVoter.ACCESS_GRANTED) + { + throw new AccessDeniedException("Access Denied"); + } + } + + return returnedObject; + } + + private AssociationRef decide(Authentication authentication, Object object, ConfigAttributeDefinition config, AssociationRef returnedObject) throws AccessDeniedException + + { + if (returnedObject == null) + { + return null; + } + + List supportedDefinitions = extractSupportedDefinitions(config); + + if (supportedDefinitions.size() == 0) + { + return returnedObject; + } + + for (ConfigAttributeDefintion cad : supportedDefinitions) + { + NodeRef testNodeRef = null; + + if (cad.parent) + { + testNodeRef = returnedObject.getSourceRef(); + } + else + { + testNodeRef = returnedObject.getTargetRef(); + } + + if (isUnfiltered(testNodeRef)) + { + continue; + } + + if (checkRead(testNodeRef) != AccessDecisionVoter.ACCESS_GRANTED) + { + throw new AccessDeniedException("Access Denied"); + } + + } + + return returnedObject; + } + + private ResultSet decide(Authentication authentication, Object object, ConfigAttributeDefinition config, PagingLuceneResultSet returnedObject) throws AccessDeniedException + + { + ResultSet raw = returnedObject.getWrapped(); + ResultSet filteredForPermissions = decide(authentication, object, config, raw); + PagingLuceneResultSet newPaging = new PagingLuceneResultSet(filteredForPermissions, returnedObject.getResultSetMetaData().getSearchParameters(), nodeService); + return newPaging; + } + + private ResultSet decide(Authentication authentication, Object object, ConfigAttributeDefinition config, ResultSet returnedObject) throws AccessDeniedException + + { + if (returnedObject == null) + { + return null; + } + + BitSet inclusionMask = new BitSet(returnedObject.length()); + FilteringResultSet filteringResultSet = new FilteringResultSet(returnedObject, inclusionMask); + + List supportedDefinitions = extractSupportedDefinitions(config); + + Integer maxSize = null; + if (returnedObject.getResultSetMetaData().getSearchParameters().getMaxItems() >= 0) + { + maxSize = new Integer(returnedObject.getResultSetMetaData().getSearchParameters().getMaxItems()); + } + if ((maxSize == null) && (returnedObject.getResultSetMetaData().getSearchParameters().getLimitBy() == LimitBy.FINAL_SIZE)) + { + maxSize = new Integer(returnedObject.getResultSetMetaData().getSearchParameters().getLimit()); + } + // Allow for skip + if ((maxSize != null) && (returnedObject.getResultSetMetaData().getSearchParameters().getSkipCount() >= 0)) + { + maxSize = new Integer(maxSize + returnedObject.getResultSetMetaData().getSearchParameters().getSkipCount()); + } + + int maxChecks = maxPermissionChecks; + if (returnedObject.getResultSetMetaData().getSearchParameters().getMaxPermissionChecks() >= 0) + { + maxChecks = returnedObject.getResultSetMetaData().getSearchParameters().getMaxPermissionChecks(); + } + + long maxCheckTime = maxPermissionCheckTimeMillis; + if (returnedObject.getResultSetMetaData().getSearchParameters().getMaxPermissionCheckTimeMillis() >= 0) + { + maxCheckTime = returnedObject.getResultSetMetaData().getSearchParameters().getMaxPermissionCheckTimeMillis(); + } + + if (supportedDefinitions.size() == 0) + { + if (maxSize == null) + { + return returnedObject; + } + else if (returnedObject.length() > maxSize.intValue()) + { + for (int i = 0; i < maxSize.intValue(); i++) + { + inclusionMask.set(i, true); + } + filteringResultSet.setResultSetMetaData(new SimpleResultSetMetaData(returnedObject.getResultSetMetaData().getLimitedBy(), PermissionEvaluationMode.EAGER, returnedObject.getResultSetMetaData() + .getSearchParameters())); + return filteringResultSet; + } + else + { + for (int i = 0; i < returnedObject.length(); i++) + { + inclusionMask.set(i, true); + } + filteringResultSet.setResultSetMetaData(new SimpleResultSetMetaData(returnedObject.getResultSetMetaData().getLimitedBy(), PermissionEvaluationMode.EAGER, returnedObject.getResultSetMetaData() + .getSearchParameters())); + return filteringResultSet; + } + } + + // record the start time + long startTimeMillis = System.currentTimeMillis(); + // set the default, unlimited resultset type + filteringResultSet.setResultSetMetaData(new SimpleResultSetMetaData(returnedObject.getResultSetMetaData().getLimitedBy(), PermissionEvaluationMode.EAGER, returnedObject.getResultSetMetaData() + .getSearchParameters())); + + for (int i = 0; i < returnedObject.length(); i++) + { + long currentTimeMillis = System.currentTimeMillis(); + if (i >= maxChecks || (currentTimeMillis - startTimeMillis) > maxCheckTime) + { + filteringResultSet.setResultSetMetaData(new SimpleResultSetMetaData(LimitBy.NUMBER_OF_PERMISSION_EVALUATIONS, PermissionEvaluationMode.EAGER, returnedObject + .getResultSetMetaData().getSearchParameters())); + break; + } + + // All permission checks must pass + inclusionMask.set(i, true); + + int parentCheckRead = checkRead(returnedObject.getChildAssocRef(i).getParentRef()); + int childCheckRead = checkRead(returnedObject.getNodeRef(i)); + + for (ConfigAttributeDefintion cad : supportedDefinitions) + { + NodeRef testNodeRef = returnedObject.getNodeRef(i); + int checkRead = childCheckRead; + if (cad.parent) + { + testNodeRef = returnedObject.getChildAssocRef(i).getParentRef(); + checkRead = parentCheckRead; + } + + if (isUnfiltered(testNodeRef)) + { + continue; + } + + if (inclusionMask.get(i) && (testNodeRef != null) && (checkRead != AccessDecisionVoter.ACCESS_GRANTED)) + { + inclusionMask.set(i, false); + } + } + + // Bug out if we are limiting by size + if ((maxSize != null) && (filteringResultSet.length() > maxSize.intValue())) + { + // Remove the last match to fix the correct size + inclusionMask.set(i, false); + filteringResultSet.setResultSetMetaData(new SimpleResultSetMetaData(LimitBy.FINAL_SIZE, PermissionEvaluationMode.EAGER, returnedObject.getResultSetMetaData() + .getSearchParameters())); + break; + } + } + return filteringResultSet; + } + + private QueryEngineResults decide(Authentication authentication, Object object, ConfigAttributeDefinition config, QueryEngineResults returnedObject) + throws AccessDeniedException + + { + Map, ResultSet> map = returnedObject.getResults(); + Map, ResultSet> answer = new HashMap, ResultSet>(map.size(), 1.0f); + + for (Set group : map.keySet()) + { + ResultSet raw = map.get(group); + ResultSet permed; + if (PagingLuceneResultSet.class.isAssignableFrom(raw.getClass())) + { + permed = decide(authentication, object, config, (PagingLuceneResultSet) raw); + } + else + { + permed = decide(authentication, object, config, raw); + } + answer.put(group, permed); + } + return new QueryEngineResults(answer); + } + + @SuppressWarnings({ "unchecked" }) + private Collection decide(Authentication authentication, Object object, ConfigAttributeDefinition config, Collection returnedObject) throws AccessDeniedException + { + if (returnedObject == null) + { + return null; + } + + List supportedDefinitions = extractSupportedDefinitions(config); + if (logger.isDebugEnabled()) + { + logger.debug("Entries are " + supportedDefinitions); + } + + if (supportedDefinitions.size() == 0) + { + return returnedObject; + } + + // Default to the system-wide values and we'll see if they need to be reduced + long targetResultCount = returnedObject.size(); + int maxPermissionChecks = Integer.MAX_VALUE; + long maxPermissionCheckTimeMillis = this.maxPermissionCheckTimeMillis; + if (returnedObject instanceof PermissionCheckCollection) + { + PermissionCheckCollection permissionCheckCollection = (PermissionCheckCollection) returnedObject; + // Get values + targetResultCount = permissionCheckCollection.getTargetResultCount(); + if (permissionCheckCollection.getCutOffAfterCount() > 0) + { + maxPermissionChecks = permissionCheckCollection.getCutOffAfterCount(); + } + if (permissionCheckCollection.getCutOffAfterTimeMs() > 0) + { + maxPermissionCheckTimeMillis = permissionCheckCollection.getCutOffAfterTimeMs(); + } + } + + // Start timer and counter for cut-off + boolean cutoff = false; + long startTimeMillis = System.currentTimeMillis(); + int count = 0; + + // Keep values explicitly + List keepValues = new ArrayList(returnedObject.size()); + + for (Object nextObject : returnedObject) + { + // if the maximum result size or time has been exceeded, then we have to remove only + long currentTimeMillis = System.currentTimeMillis(); + + // NOTE: for reference - the "maxPermissionChecks" has never been honoured by this loop (since previously the count was not being incremented) + if (count >= targetResultCount) + { + // We have enough results. We stop without cutoff. + break; + } + else if (count >= maxPermissionChecks) + { + // We have been cut off by count + cutoff = true; + if (logger.isDebugEnabled()) + { + logger.debug("decide (collection) cut-off: " + count + " checks exceeded " + maxPermissionChecks + " checks"); + } + break; + } + else if ((currentTimeMillis - startTimeMillis) > maxPermissionCheckTimeMillis) + { + // We have been cut off by time + cutoff = true; + if (logger.isDebugEnabled()) + { + logger.debug("decide (collection) cut-off: " + (currentTimeMillis - startTimeMillis) + "ms exceeded " + maxPermissionCheckTimeMillis + "ms"); + } + break; + } + + boolean allowed = true; + for (ConfigAttributeDefintion cad : supportedDefinitions) + { + if (cad.mode.equalsIgnoreCase("FilterNode")) + { + NodeRef testNodeRef = null; + if (cad.parent) + { + if (StoreRef.class.isAssignableFrom(nextObject.getClass())) + { + // Will be allowed + testNodeRef = null; + } + else if (NodeRef.class.isAssignableFrom(nextObject.getClass())) + { + testNodeRef = nodeService.getPrimaryParent((NodeRef) nextObject).getParentRef(); + } + else if (ChildAssociationRef.class.isAssignableFrom(nextObject.getClass())) + { + testNodeRef = ((ChildAssociationRef) nextObject).getParentRef(); + } + else if (AssociationRef.class.isAssignableFrom(nextObject.getClass())) + { + testNodeRef = ((AssociationRef) nextObject).getSourceRef(); + } + else if (PermissionCheckValue.class.isAssignableFrom(nextObject.getClass())) + { + NodeRef nodeRef = ((PermissionCheckValue) nextObject).getNodeRef(); + testNodeRef = nodeService.getPrimaryParent(nodeRef).getParentRef(); + } + else + { + throw new ACLEntryVoterException("The specified parameter is recognized: " + nextObject.getClass()); + } + } + else + { + if (StoreRef.class.isAssignableFrom(nextObject.getClass())) + { + testNodeRef = nodeService.getRootNode((StoreRef) nextObject); + } + else if (NodeRef.class.isAssignableFrom(nextObject.getClass())) + { + testNodeRef = (NodeRef) nextObject; + } + else if (ChildAssociationRef.class.isAssignableFrom(nextObject.getClass())) + { + testNodeRef = ((ChildAssociationRef) nextObject).getChildRef(); + } + else if (AssociationRef.class.isAssignableFrom(nextObject.getClass())) + { + testNodeRef = ((AssociationRef) nextObject).getTargetRef(); + } + else if (PermissionCheckValue.class.isAssignableFrom(nextObject.getClass())) + { + testNodeRef = ((PermissionCheckValue) nextObject).getNodeRef(); + } + else + { + throw new ACLEntryVoterException("The specified parameter is recognized: " + nextObject.getClass()); + } + } + + if (logger.isDebugEnabled()) + { + logger.debug("\t" + cad.typeString + " test on " + testNodeRef + " from " + nextObject.getClass().getName()); + } + + if (isUnfiltered(testNodeRef)) // Null allows + { + continue; // Continue to next ConfigAttributeDefintion + } + + if (allowed && (testNodeRef != null) && (checkRead(testNodeRef) != AccessDecisionVoter.ACCESS_GRANTED)) + { + allowed = false; + break; // No point evaluating more ConfigAttributeDefintions + } + } + } + + // Failure or success, increase the count + count++; + + if (allowed) + { + keepValues.add(nextObject); + } + } + // Work out how many were left unchecked (for whatever reason) + int sizeOriginal = returnedObject.size(); + int checksRemaining = sizeOriginal - count; + // Note: There are use-cases where unmodifiable collections are passing through. + // So make sure that the collection needs modification at all + if (keepValues.size() < sizeOriginal) + { + // There are values that need to be removed. We have to modify the collection. + try + { + returnedObject.clear(); + returnedObject.addAll(keepValues); + } + catch (UnsupportedOperationException e) + { + throw new AccessDeniedException("Permission-checked list must be modifiable", e); + } + } + + // Attach the extra permission-check data to the collection + return PermissionCheckedCollectionMixin.create(returnedObject, cutoff, checksRemaining, sizeOriginal); + } + + private Object[] decide(Authentication authentication, Object object, ConfigAttributeDefinition config, Object[] returnedObject) throws AccessDeniedException + { + // Assumption: value is not null + BitSet incudedSet = new BitSet(returnedObject.length); + + List supportedDefinitions = extractSupportedDefinitions(config); + + if (supportedDefinitions.size() == 0) + { + return returnedObject; + } + + for (int i = 0, l = returnedObject.length; i < l; i++) + { + Object current = returnedObject[i]; + + int parentReadCheck = checkRead(getParentReadCheckNode(current)); + int childReadChek = checkRead(getChildReadCheckNode(current)); + + for (ConfigAttributeDefintion cad : supportedDefinitions) + { + incudedSet.set(i, true); + NodeRef testNodeRef = null; + if (cad.parent) + { + if (StoreRef.class.isAssignableFrom(current.getClass())) + { + testNodeRef = null; + } + else if (NodeRef.class.isAssignableFrom(current.getClass())) + { + testNodeRef = nodeService.getPrimaryParent((NodeRef) current).getParentRef(); + } + else if (ChildAssociationRef.class.isAssignableFrom(current.getClass())) + { + testNodeRef = ((ChildAssociationRef) current).getParentRef(); + } + else if (PermissionCheckValue.class.isAssignableFrom(current.getClass())) + { + NodeRef nodeRef = ((PermissionCheckValue) current).getNodeRef(); + testNodeRef = nodeService.getPrimaryParent(nodeRef).getParentRef(); + } + else + { + throw new ACLEntryVoterException("The specified parameter is recognized: " + current.getClass()); + } + } + else + { + if (StoreRef.class.isAssignableFrom(current.getClass())) + { + testNodeRef = nodeService.getRootNode((StoreRef) current); + } + else if (NodeRef.class.isAssignableFrom(current.getClass())) + { + testNodeRef = (NodeRef) current; + } + else if (ChildAssociationRef.class.isAssignableFrom(current.getClass())) + { + testNodeRef = ((ChildAssociationRef) current).getChildRef(); + } + else if (PermissionCheckValue.class.isAssignableFrom(current.getClass())) + { + testNodeRef = ((PermissionCheckValue) current).getNodeRef(); + } + else + { + throw new ACLEntryVoterException("The specified parameter is recognized: " + current.getClass()); + } + } + + if (logger.isDebugEnabled()) + { + logger.debug("\t" + cad.typeString + " test on " + testNodeRef + " from " + current.getClass().getName()); + } + + if (isUnfiltered(testNodeRef)) + { + continue; + } + + int readCheck = childReadChek; + if (cad.parent == true) + { + readCheck = parentReadCheck; + } + + if (incudedSet.get(i) && (testNodeRef != null) && (readCheck != AccessDecisionVoter.ACCESS_GRANTED)) + { + incudedSet.set(i, false); + } + + } + } + + if (incudedSet.cardinality() == returnedObject.length) + { + return returnedObject; + } + else + { + Object[] answer = new Object[incudedSet.cardinality()]; + for (int i = incudedSet.nextSetBit(0), p = 0; i >= 0; i = incudedSet.nextSetBit(++i), p++) + { + answer[p] = returnedObject[i]; + } + return answer; + } + } + + private NodeRef getParentReadCheckNode(Object current) + { + NodeRef testNodeRef = null; + if (StoreRef.class.isAssignableFrom(current.getClass())) + { + testNodeRef = null; + } + else if (NodeRef.class.isAssignableFrom(current.getClass())) + { + testNodeRef = nodeService.getPrimaryParent((NodeRef) current).getParentRef(); + } + else if (ChildAssociationRef.class.isAssignableFrom(current.getClass())) + { + testNodeRef = ((ChildAssociationRef) current).getParentRef(); + } + else if (PermissionCheckValue.class.isAssignableFrom(current.getClass())) + { + NodeRef nodeRef = ((PermissionCheckValue) current).getNodeRef(); + testNodeRef = nodeService.getPrimaryParent(nodeRef).getParentRef(); + } + else + { + throw new ACLEntryVoterException("The specified array is not of NodeRef or ChildAssociationRef"); + } + return testNodeRef; + } + + private NodeRef getChildReadCheckNode(Object current) + { + NodeRef testNodeRef = null; + if (StoreRef.class.isAssignableFrom(current.getClass())) + { + testNodeRef = nodeService.getRootNode((StoreRef) current); + } + else if (NodeRef.class.isAssignableFrom(current.getClass())) + { + testNodeRef = (NodeRef) current; + } + else if (ChildAssociationRef.class.isAssignableFrom(current.getClass())) + { + testNodeRef = ((ChildAssociationRef) current).getChildRef(); + } + else if (PermissionCheckValue.class.isAssignableFrom(current.getClass())) + { + testNodeRef = ((PermissionCheckValue) current).getNodeRef(); + } + else + { + throw new ACLEntryVoterException("The specified array is not of NodeRef or ChildAssociationRef"); + } + return testNodeRef; + } + + @SuppressWarnings({"unchecked" }) + private Map decide(Authentication authentication, Object object, ConfigAttributeDefinition config, Map returnedObject) throws AccessDeniedException + { + if (returnedObject.containsKey(RecordsManagementModel.PROP_HOLD_REASON)) + { + HashMap filtered = new HashMap(); + filtered.putAll(returnedObject); + // get the node ref from the properties or delete + String protocol = DefaultTypeConverter.INSTANCE.convert(String.class, filtered.get(ContentModel.PROP_STORE_PROTOCOL)); + String identifier = DefaultTypeConverter.INSTANCE.convert(String.class, filtered.get(ContentModel.PROP_STORE_IDENTIFIER)); + String uuid = DefaultTypeConverter.INSTANCE.convert(String.class, filtered.get(ContentModel.PROP_NODE_UUID)); + StoreRef storeRef = new StoreRef(protocol, identifier); + NodeRef nodeRef = new NodeRef(storeRef, uuid); + if ((nodeRef == null) || (permissionService.hasPermission(rmService.getFilePlan(nodeRef), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE) != AccessStatus.ALLOWED)) + { + filtered.remove(RecordsManagementModel.PROP_HOLD_REASON); + } + return filtered; + } + else + { + return returnedObject; + } + } + + private class ConfigAttributeDefintion + { + + String typeString; + + String mode; + + boolean parent = false; + + ConfigAttributeDefintion(ConfigAttribute attr) + { + + StringTokenizer st = new StringTokenizer(attr.getAttribute(), ".", false); + typeString = st.nextToken(); + if (!(typeString.equals(AFTER_RM))) + { + throw new ACLEntryVoterException("Invalid type: must be AFTER_RM"); + } + mode = st.nextToken(); + + if (st.hasMoreElements()) + { + parent = true; + } + } + } + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/RMEntryVoter.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/RMEntryVoter.java new file mode 100644 index 0000000000..473d87ce3c --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/RMEntryVoter.java @@ -0,0 +1,1091 @@ +/* + * 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.module.org_alfresco_module_rm.capability; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.StringTokenizer; + +import net.sf.acegisecurity.Authentication; +import net.sf.acegisecurity.ConfigAttribute; +import net.sf.acegisecurity.ConfigAttributeDefinition; +import net.sf.acegisecurity.vote.AccessDecisionVoter; + +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction; +import org.alfresco.module.org_alfresco_module_rm.capability.group.CreateCapability; +import org.alfresco.module.org_alfresco_module_rm.capability.group.UpdateCapability; +import org.alfresco.module.org_alfresco_module_rm.capability.group.UpdatePropertiesCapability; +import org.alfresco.module.org_alfresco_module_rm.capability.impl.MoveRecordsCapability; +import org.alfresco.module.org_alfresco_module_rm.caveat.RMCaveatConfigComponent; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.permissions.impl.SimplePermissionReference; +import org.alfresco.repo.security.permissions.impl.acegi.ACLEntryVoterException; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.AssociationRef; +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.cmr.search.SearchService; +import org.alfresco.service.cmr.security.OwnableService; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.EqualsHelper; +import org.aopalliance.intercept.MethodInvocation; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; + +public class RMEntryVoter extends RMSecurityCommon + implements AccessDecisionVoter, InitializingBean, ApplicationContextAware +{ + private static Log logger = LogFactory.getLog(RMEntryVoter.class); + + private static final String RM = "RM"; + private static final String RM_ALLOW = "RM_ALLOW"; + private static final String RM_DENY = "RM_DENY"; + private static final String RM_CAP = "RM_CAP"; + private static final String RM_ABSTAIN = "RM_ABSTAIN"; + private static final String RM_QUERY = "RM_QUERY"; + + private NamespacePrefixResolver nspr; + private NodeService nodeService; + private PermissionService permissionService; + private RMCaveatConfigComponent caveatConfigComponent; + private DictionaryService dictionaryService; + private RecordsManagementService recordsManagementService; + private DispositionService dispositionService; + private SearchService searchService; + private OwnableService ownableService; + + private CapabilityService capabilityService; + + private static HashMap policies = new HashMap(); + + private HashSet protectedProperties = new HashSet(); + + private HashSet protectedAspects = new HashSet(); + + + static + { + policies.put("Read", new ReadPolicy()); + policies.put("Create", new CreatePolicy()); + policies.put("Move", new MovePolicy()); + policies.put("Update", new UpdatePolicy()); + policies.put("Delete", new DeletePolicy()); + policies.put("UpdateProperties", new UpdatePropertiesPolicy()); + policies.put("Assoc", new AssocPolicy()); + policies.put("WriteContent", new WriteContentPolicy()); + policies.put("Capability", new CapabilityPolicy()); + policies.put("Declare", new DeclarePolicy()); + policies.put("ReadProperty", new ReadPropertyPolicy()); + + // restrictedProperties.put(RecordsManagementModel.PROP_IS_CLOSED, value) + + } + + /** + * Set the permission service + * + * @param permissionService + */ + public void setPermissionService(PermissionService permissionService) + { + this.permissionService = permissionService; + } + + /** + * Set the node service + * + * @param nodeService + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * @param capabilityService capability service + */ + public void setCapabilityService(CapabilityService capabilityService) + { + this.capabilityService = capabilityService; + } + + /** + * Get the search service + * @return search service + */ + public SearchService getSearchService() + { + if (searchService == null) + { + searchService = (SearchService)applicationContext.getBean("SearchService"); + } + return searchService; + } + + /** + * @return + */ + public OwnableService getOwnableService() + { + if (ownableService == null) + { + ownableService = (OwnableService)applicationContext.getBean("ownableService"); + } + return ownableService; + } + + /** + * Set the name space prefix resolver + * + * @param nspr + */ + public void setNamespacePrefixResolver(NamespacePrefixResolver nspr) + { + this.nspr = nspr; + } + + public void setCaveatConfigComponent(RMCaveatConfigComponent caveatConfigComponent) + { + this.caveatConfigComponent = caveatConfigComponent; + } + + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + public boolean supports(ConfigAttribute attribute) + { + if ((attribute.getAttribute() != null) + && (attribute.getAttribute().equals(RM_ABSTAIN) + || attribute.getAttribute().equals(RM_QUERY) || attribute.getAttribute().equals(RM_ALLOW) || attribute.getAttribute().equals(RM_DENY) + || attribute.getAttribute().startsWith(RM_CAP) || attribute.getAttribute().startsWith(RM))) + { + return true; + } + else + { + return false; + } + } + + @SuppressWarnings("unchecked") + public boolean supports(Class clazz) + { + return (MethodInvocation.class.isAssignableFrom(clazz)); + } + + public void addProtectedProperties(Set properties) + { + protectedProperties.addAll(properties); + } + + public void addProtectedAspects(Set aspects) + { + protectedAspects.addAll(aspects); + } + + public Set getProtectedProperties() + { + return Collections.unmodifiableSet(protectedProperties); + } + + public Set getProtetcedAscpects() + { + return Collections.unmodifiableSet(protectedAspects); + } + + public int vote(Authentication authentication, Object object, ConfigAttributeDefinition config) + { + if (logger.isDebugEnabled()) + { + MethodInvocation mi = (MethodInvocation) object; + logger.debug("Method: " + mi.getMethod().toString()); + } + // The system user can do anything + if (AuthenticationUtil.isRunAsUserTheSystemUser()) + { + if (logger.isDebugEnabled()) + { + logger.debug("Access granted for the system user"); + } + return AccessDecisionVoter.ACCESS_GRANTED; + } + + List supportedDefinitions = extractSupportedDefinitions(config); + + // No RM definitions so we do not vote + if (supportedDefinitions.size() == 0) + { + return AccessDecisionVoter.ACCESS_ABSTAIN; + } + + MethodInvocation invocation = (MethodInvocation) object; + + Method method = invocation.getMethod(); + Class[] params = method.getParameterTypes(); + + // If there are only capability (RM_CAP) and policy (RM) entries non must deny + // If any abstain we deny + // All present must vote to allow unless an explicit direction comes first (e.g. RM_ALLOW) + + for (ConfigAttributeDefintion cad : supportedDefinitions) + { + // Whatever is found first takes precedence + if (cad.typeString.equals(RM_DENY)) + { + return AccessDecisionVoter.ACCESS_DENIED; + } + else if (cad.typeString.equals(RM_ABSTAIN)) + { + return AccessDecisionVoter.ACCESS_ABSTAIN; + } + else if (cad.typeString.equals(RM_ALLOW)) + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + // RM_QUERY is a special case - the entry is allowed and filtering sorts out the results + // It is distinguished from RM_ALLOW so query may have additional behaviour in the future + else if (cad.typeString.equals(RM_QUERY)) + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + // Ignore config that references method arguments that do not exist + // Arguably we should deny here but that requires a full impact analysis + // These entries effectively abstain + else if (((cad.parameters.get(0) != null) && (cad.parameters.get(0) >= invocation.getArguments().length)) + || ((cad.parameters.get(1) != null) && (cad.parameters.get(1) >= invocation.getArguments().length))) + { + continue; + } + else if (cad.typeString.equals(RM_CAP)) + { + switch(checkCapability(invocation, params, cad)) + { + case AccessDecisionVoter.ACCESS_DENIED: + return AccessDecisionVoter.ACCESS_DENIED; + case AccessDecisionVoter.ACCESS_ABSTAIN: + if(logger.isDebugEnabled()) + { + if(logger.isTraceEnabled()) + { + logger.trace("Capability " + cad.required + " abstained for " + invocation.getMethod(), new IllegalStateException()); + } + else + { + logger.debug("Capability " + cad.required + " abstained for " + invocation.getMethod()); + } + } + // abstain denies + return AccessDecisionVoter.ACCESS_DENIED; + case AccessDecisionVoter.ACCESS_GRANTED: + break; + } + } + else if (cad.typeString.equals(RM)) + { + switch(checkPolicy(invocation, params, cad)) + { + case AccessDecisionVoter.ACCESS_DENIED: + return AccessDecisionVoter.ACCESS_DENIED; + case AccessDecisionVoter.ACCESS_ABSTAIN: + if(logger.isDebugEnabled()) + { + if(logger.isTraceEnabled()) + { + logger.trace("Policy " + cad.policyName + " abstained for " + invocation.getMethod(), new IllegalStateException()); + } + else + { + logger.debug("Policy " + cad.policyName + " abstained for " + invocation.getMethod()); + } + } + // abstain denies + return AccessDecisionVoter.ACCESS_DENIED; + case AccessDecisionVoter.ACCESS_GRANTED: + break; + } + } + } + + // all voted to allow + + return AccessDecisionVoter.ACCESS_GRANTED; + + } + + private int checkCapability(MethodInvocation invocation, Class[] params, ConfigAttributeDefintion cad) + { + NodeRef testNodeRef = getTestNode(getNodeService(), getRecordsManagementService(), invocation, params, cad.parameters.get(0), cad.parent); + if (testNodeRef == null) + { + return AccessDecisionVoter.ACCESS_ABSTAIN; + } + Capability capability = capabilityService.getCapability(cad.required.getName()); + if (capability == null) + { + return AccessDecisionVoter.ACCESS_DENIED; + } + return capability.hasPermissionRaw(testNodeRef); + + } + + private static QName getType(NodeService nodeService, MethodInvocation invocation, Class[] params, int position, boolean parent) + { + if (QName.class.isAssignableFrom(params[position])) + { + if (invocation.getArguments()[position] != null) + { + QName qname = (QName) invocation.getArguments()[position]; + return qname; + } + } + else if (NodeRef.class.isAssignableFrom(params[position])) + { + if (invocation.getArguments()[position] != null) + { + NodeRef nodeRef = (NodeRef) invocation.getArguments()[position]; + return nodeService.getType(nodeRef); + } + } + + return null; + } + + private static QName getQName(MethodInvocation invocation, Class[] params, int position) + { + if (QName.class.isAssignableFrom(params[position])) + { + if (invocation.getArguments()[position] != null) + { + QName qname = (QName) invocation.getArguments()[position]; + return qname; + } + } + throw new ACLEntryVoterException("Unknown type"); + } + + private static Serializable getProperty(MethodInvocation invocation, Class[] params, int position) + { + if (invocation.getArguments()[position] == null) + { + return null; + } + if (Serializable.class.isAssignableFrom(params[position])) + { + if (invocation.getArguments()[position] != null) + { + Serializable property = (Serializable) invocation.getArguments()[position]; + return property; + } + } + throw new ACLEntryVoterException("Unknown type"); + } + + private static Map getProperties(MethodInvocation invocation, Class[] params, int position) + { + if (invocation.getArguments()[position] == null) + { + return null; + } + if (Map.class.isAssignableFrom(params[position])) + { + if (invocation.getArguments()[position] != null) + { + Map properties = (Map) invocation.getArguments()[position]; + return properties; + } + } + throw new ACLEntryVoterException("Unknown type"); + } + + private static NodeRef getTestNode(NodeService nodeService, RecordsManagementService rmService, MethodInvocation invocation, Class[] params, int position, boolean parent) + { + NodeRef testNodeRef = null; + if (position < 0) + { + // Test against the fileplan root node + List rmRoots = rmService.getFilePlans(); + if (rmRoots.size() != 0) + { + // TODO for now we can take the first one as we only support a single rm site + testNodeRef = rmRoots.get(0); + + if (logger.isDebugEnabled()) + { + logger.debug("\tPermission test against the rm root node " + nodeService.getPath(testNodeRef)); + } + } + } + else if (StoreRef.class.isAssignableFrom(params[position])) + { + if (invocation.getArguments()[position] != null) + { + if (logger.isDebugEnabled()) + { + logger.debug("\tPermission test against the store - using permissions on the root node"); + } + StoreRef storeRef = (StoreRef) invocation.getArguments()[position]; + if (nodeService.exists(storeRef)) + { + testNodeRef = nodeService.getRootNode(storeRef); + } + } + } + else if (NodeRef.class.isAssignableFrom(params[position])) + { + testNodeRef = (NodeRef) invocation.getArguments()[position]; + if (parent) + { + testNodeRef = nodeService.getPrimaryParent(testNodeRef).getParentRef(); + if (logger.isDebugEnabled()) + { + if (nodeService.exists(testNodeRef)) + { + logger.debug("\tPermission test for parent on node " + nodeService.getPath(testNodeRef)); + } + else + { + logger.debug("\tPermission test for parent on non-existing node " + testNodeRef); + } + logger.debug("\tPermission test for parent on node " + nodeService.getPath(testNodeRef)); + } + } + else + { + if (logger.isDebugEnabled()) + { + if (nodeService.exists(testNodeRef)) + { + logger.debug("\tPermission test on node " + nodeService.getPath(testNodeRef)); + } + else + { + logger.debug("\tPermission test on non-existing node " + testNodeRef); + } + } + } + } + else if (ChildAssociationRef.class.isAssignableFrom(params[position])) + { + if (invocation.getArguments()[position] != null) + { + if (parent) + { + testNodeRef = ((ChildAssociationRef) invocation.getArguments()[position]).getParentRef(); + } + else + { + testNodeRef = ((ChildAssociationRef) invocation.getArguments()[position]).getChildRef(); + } + if (logger.isDebugEnabled()) + { + if (nodeService.exists(testNodeRef)) + { + logger.debug("\tPermission test on node " + nodeService.getPath(testNodeRef)); + } + else + { + logger.debug("\tPermission test on non-existing node " + testNodeRef); + } + } + } + } + else if (AssociationRef.class.isAssignableFrom(params[position])) + { + if (invocation.getArguments()[position] != null) + { + if (parent) + { + testNodeRef = ((AssociationRef) invocation.getArguments()[position]).getSourceRef(); + } + else + { + testNodeRef = ((AssociationRef) invocation.getArguments()[position]).getTargetRef(); + } + if (logger.isDebugEnabled()) + { + if (nodeService.exists(testNodeRef)) + { + logger.debug("\tPermission test on node " + nodeService.getPath(testNodeRef)); + } + else + { + logger.debug("\tPermission test on non-existing node " + testNodeRef); + } + } + } + } + return testNodeRef; + } + + private int checkPolicy(MethodInvocation invocation, Class[] params, ConfigAttributeDefintion cad) + { + Policy policy = policies.get(cad.policyName); + if (policy == null) + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + else + { + return policy.evaluate(this.nodeService, this.recordsManagementService, this.capabilityService, invocation, params, cad); + } + } + + public void afterPropertiesSet() throws Exception + { + // TODO Auto-generated method stub + + } + + private List extractSupportedDefinitions(ConfigAttributeDefinition config) + { + List definitions = new ArrayList(2); + Iterator iter = config.getConfigAttributes(); + + while (iter.hasNext()) + { + ConfigAttribute attr = (ConfigAttribute) iter.next(); + + if (this.supports(attr)) + { + definitions.add(new ConfigAttributeDefintion(attr)); + } + + } + return definitions; + } + + /** + * @return the nodeService + */ + public NodeService getNodeService() + { + return nodeService; + } + + /** + * @return the permissionService + */ + public PermissionService getPermissionService() + { + return permissionService; + } + + /** + * @return the caveatConfigService + */ + public RMCaveatConfigComponent getCaveatConfigComponent() + { + return caveatConfigComponent; + } + + /** + * @param recordsManagementService + * the recordsManagementService to set + */ + public void setRecordsManagementService(RecordsManagementService recordsManagementService) + { + this.recordsManagementService = recordsManagementService; + } + + /** + * @return the recordsManagementService + */ + public RecordsManagementService getRecordsManagementService() + { + return recordsManagementService; + } + + public void setDispositionService(DispositionService dispositionService) + { + this.dispositionService = dispositionService; + } + + public DispositionService getDispositionService() + { + return dispositionService; + } + + /** + * @return the dictionaryService + */ + public DictionaryService getDictionaryService() + { + return dictionaryService; + } + + public boolean isProtectedAspect(NodeRef nodeRef, QName aspectQName) + { + if(protectedAspects.contains(aspectQName)) + { + for(Capability capability : capabilityService.getCapabilities()) + { + for(RecordsManagementAction action : capability.getActions()) + { + if(action.getProtectedAspects().contains(aspectQName)) + { + if(action.isExecutable(nodeRef, null)) + { + return false; + } + } + } + } + return true; + } + else + { + return false; + } + } + + public boolean isProtectedProperty(NodeRef nodeRef, QName propertyQName) + { + if(protectedProperties.contains(propertyQName)) + { + for(Capability capability : capabilityService.getCapabilities()) + { + for(RecordsManagementAction action : capability.getActions()) + { + if(action.getProtectedProperties().contains(propertyQName)) + { + if(action.isExecutable(nodeRef, null)) + { + return false; + } + } + } + } + return true; + } + else + { + return false; + } + } + + public boolean includesProtectedPropertyChange(NodeRef nodeRef, Map properties) + { + Map originals = nodeService.getProperties(nodeRef); + for (QName test : properties.keySet()) + { + if (isProtectedProperty(nodeRef, test)) + { + if (!EqualsHelper.nullSafeEquals(originals.get(test), properties.get(test))) + { + return true; + } + } + } + return false; + } + + private class ConfigAttributeDefintion + { + String typeString; + + String policyName; + + SimplePermissionReference required; + + HashMap parameters = new HashMap(2, 1.0f); + + boolean parent = false; + + ConfigAttributeDefintion(ConfigAttribute attr) + { + StringTokenizer st = new StringTokenizer(attr.getAttribute(), ".", false); + if (st.countTokens() < 1) + { + throw new ACLEntryVoterException("There must be at least one token in a config attribute"); + } + typeString = st.nextToken(); + + if (!(typeString.equals(RM) || typeString.equals(RM_ALLOW) || typeString.equals(RM_CAP) || typeString.equals(RM_DENY) || typeString.equals(RM_QUERY) || typeString + .equals(RM_ABSTAIN))) + { + throw new ACLEntryVoterException("Invalid type: must be ACL_NODE, ACL_PARENT or ACL_ALLOW"); + } + + if (typeString.equals(RM)) + { + policyName = st.nextToken(); + int position = 0; + while (st.hasMoreElements()) + { + String numberString = st.nextToken(); + Integer value = Integer.parseInt(numberString); + parameters.put(position, value); + position++; + } + } + else if (typeString.equals(RM_CAP)) + { + String numberString = st.nextToken(); + String qNameString = st.nextToken(); + String permissionString = st.nextToken(); + + Integer value = Integer.parseInt(numberString); + parameters.put(0, value); + + QName qName = QName.createQName(qNameString, nspr); + + required = SimplePermissionReference.getPermissionReference(qName, permissionString); + + if (st.hasMoreElements()) + { + parent = true; + } + } + } + } + + interface Policy + { + /** + * + * @param nodeService + * @param rmService + * @param capabilitiesService + * @param invocation + * @param params + * @param cad + * @return + */ + int evaluate( + NodeService nodeService, + RecordsManagementService rmService, + CapabilityService capabilitiesService, + MethodInvocation invocation, + Class[] params, + ConfigAttributeDefintion cad); + } + + private static class ReadPolicy implements Policy + { + + public int evaluate( + NodeService nodeService, + RecordsManagementService rmService, + CapabilityService capabilityService, + MethodInvocation invocation, + Class[] params, + ConfigAttributeDefintion cad) + { + NodeRef testNodeRef = getTestNode(nodeService, rmService, invocation, params, cad.parameters.get(0), cad.parent); + return capabilityService.getCapability(RMPermissionModel.VIEW_RECORDS).evaluate(testNodeRef); + } + + } + + private static class CreatePolicy implements Policy + { + + public int evaluate( + NodeService nodeService, + RecordsManagementService rmService, + CapabilityService capabilityService, + MethodInvocation invocation, + Class[] params, + ConfigAttributeDefintion cad) + { + + NodeRef destination = getTestNode(nodeService, rmService, invocation, params, cad.parameters.get(0), cad.parent); + QName type = getType(nodeService, invocation, params, cad.parameters.get(1), cad.parent); + // linkee is not null for creating secondary child assocs + NodeRef linkee = getTestNode(nodeService, rmService, invocation, params, cad.parameters.get(1), cad.parent); + QName assocType = null; + if(cad.parameters.size() > 2) + { + assocType = getType(nodeService, invocation, params, cad.parameters.get(2), cad.parent); + } + + return ((CreateCapability)capabilityService.getCapability("Create")).evaluate(destination, linkee, type, assocType); + } + + } + + private static class MovePolicy implements Policy + { + + public int evaluate( + NodeService nodeService, + RecordsManagementService rmService, + CapabilityService capabilityService, + MethodInvocation invocation, + Class[] params, + ConfigAttributeDefintion cad) + { + + NodeRef movee = null; + if (cad.parameters.get(0) > -1) + { + movee = getTestNode(nodeService, rmService, invocation, params, cad.parameters.get(0), cad.parent); + } + + NodeRef destination = null; + if (cad.parameters.get(1) > -1) + { + destination = getTestNode(nodeService, rmService, invocation, params, cad.parameters.get(1), cad.parent); + } + + if ((movee != null) && (destination != null)) + { + return ((MoveRecordsCapability)capabilityService.getCapability(RMPermissionModel.MOVE_RECORDS)).evaluate(movee, destination); + } + else + { + return AccessDecisionVoter.ACCESS_DENIED; + } + + } + } + + private static class UpdatePolicy implements Policy + { + + public int evaluate( + NodeService nodeService, + RecordsManagementService rmService, + CapabilityService capabilityService, + MethodInvocation invocation, + Class[] params, + ConfigAttributeDefintion cad) + { + NodeRef updatee = getTestNode(nodeService, rmService, invocation, params, cad.parameters.get(0), cad.parent); + QName aspectQName = null; + if (cad.parameters.size() > 1) + { + if (cad.parameters.get(1) > -1) + { + aspectQName = getQName(invocation, params, cad.parameters.get(1)); + } + } + Map properties = null; + if (cad.parameters.size() > 2) + { + if (cad.parameters.get(2) > -1) + { + properties = getProperties(invocation, params, cad.parameters.get(2)); + } + } + + UpdateCapability updateCapability = (UpdateCapability)capabilityService.getCapability("Update"); + return updateCapability.evaluate(updatee, aspectQName, properties); + } + + } + + private static class DeletePolicy implements Policy + { + + public int evaluate( + NodeService nodeService, + RecordsManagementService rmService, + CapabilityService capabilityService, + MethodInvocation invocation, + Class[] params, + ConfigAttributeDefintion cad) + { + NodeRef deletee = null; + if (cad.parameters.get(0) > -1) + { + deletee = getTestNode(nodeService, rmService, invocation, params, cad.parameters.get(0), cad.parent); + } + if (deletee != null) + { + + return capabilityService.getCapability("Delete").evaluate(deletee); + + } + else + { + return AccessDecisionVoter.ACCESS_DENIED; + } + } + + } + + private static class UpdatePropertiesPolicy implements Policy + { + public int evaluate( + NodeService nodeService, + RecordsManagementService rmService, + CapabilityService capabilityService, + MethodInvocation invocation, + Class[] params, + ConfigAttributeDefintion cad) + { + NodeRef updatee = getTestNode(nodeService, rmService, invocation, params, cad.parameters.get(0), cad.parent); + Map properties; + if (QName.class.isAssignableFrom(params[cad.parameters.get(1)])) + { + // single update/delete + // We have a specific property + QName propertyQName = getQName(invocation, params, cad.parameters.get(1)); + properties = new HashMap(1, 1.0f); + if (cad.parameters.size() > 2) + { + properties.put(propertyQName, getProperty(invocation, params, cad.parameters.get(2))); + } + else + { + properties.put(propertyQName, null); + } + } + else + { + properties = getProperties(invocation, params, cad.parameters.get(1)); + } + + return ((UpdatePropertiesCapability)capabilityService.getCapability("UpdateProperties")).evaluate(updatee, properties); + } + + } + + private static class AssocPolicy implements Policy + { + + public int evaluate( + NodeService nodeService, + RecordsManagementService rmService, + CapabilityService capabilityService, + MethodInvocation invocation, + Class[] params, + ConfigAttributeDefintion cad) + { + Policy policy = policies.get("Read"); + if (policy == null) + { + return AccessDecisionVoter.ACCESS_DENIED; + } + else + { + return policy.evaluate(nodeService, rmService, capabilityService, invocation, params, cad); + } + } + + } + + private static class WriteContentPolicy implements Policy + { + + public int evaluate( + NodeService nodeService, + RecordsManagementService rmService, + CapabilityService capabilityService, + MethodInvocation invocation, + Class[] params, + ConfigAttributeDefintion cad) + { + NodeRef updatee = getTestNode(nodeService, rmService, invocation, params, cad.parameters.get(0), cad.parent); + return capabilityService.getCapability("WriteContent").evaluate(updatee); + } + + } + + private static class CapabilityPolicy implements Policy + { + + public int evaluate( + NodeService nodeService, + RecordsManagementService rmService, + CapabilityService capabilityService, + MethodInvocation invocation, + Class[] params, + ConfigAttributeDefintion cad) + { + NodeRef assignee = getTestNode(nodeService, rmService, invocation, params, cad.parameters.get(0), cad.parent); + return capabilityService.getCapability(RMPermissionModel.MANAGE_ACCESS_CONTROLS).evaluate(assignee); + } + + } + + private static class DeclarePolicy implements Policy + { + + public int evaluate( + NodeService nodeService, + RecordsManagementService rmService, + CapabilityService capabilityService, + MethodInvocation invocation, + Class[] params, + ConfigAttributeDefintion cad) + { + NodeRef declaree = getTestNode(nodeService, rmService, invocation, params, cad.parameters.get(0), cad.parent); + return capabilityService.getCapability("Declare").evaluate(declaree); + } + + } + + private static class ReadPropertyPolicy implements Policy + { + + public int evaluate( + NodeService nodeService, + RecordsManagementService rmService, + CapabilityService capabilityService, + MethodInvocation invocation, + Class[] params, + ConfigAttributeDefintion cad) + { + NodeRef nodeRef = getTestNode(nodeService, rmService, invocation, params, cad.parameters.get(0), cad.parent); + QName propertyQName = getQName(invocation, params, cad.parameters.get(1)); + if(propertyQName.equals(RecordsManagementModel.PROP_HOLD_REASON)) + { + return capabilityService.getCapability(RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE).evaluate(nodeRef); + } + else + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + } + + } + + private ApplicationContext applicationContext; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) + throws BeansException + { + this.applicationContext = applicationContext; + } + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/RMPermissionModel.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/RMPermissionModel.java new file mode 100644 index 0000000000..5dafa88c16 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/RMPermissionModel.java @@ -0,0 +1,175 @@ +/* + * 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.module.org_alfresco_module_rm.capability; + +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.repo.security.permissions.impl.SimplePermissionReference; + +/** + * Capability constants for the RM Permission Model + * + * @author andyh + */ +public interface RMPermissionModel +{ + // Assignment of Filing + + public static final String FILING = "Filing"; + + public static final String READ_RECORDS = "ReadRecords"; + + public static final String FILE_RECORDS = "FileRecords"; + + // Roles + + public static final String ROLE_NAME_USER = "User"; + public static final String ROLE_USER = SimplePermissionReference.getPermissionReference(RecordsManagementModel.ASPECT_FILE_PLAN_COMPONENT, ROLE_NAME_USER).toString(); + + public static final String ROLE_NAME_POWER_USER = "PowerUser"; + public static final String ROLE_POWER_USER = SimplePermissionReference.getPermissionReference(RecordsManagementModel.ASPECT_FILE_PLAN_COMPONENT, ROLE_NAME_POWER_USER).toString(); + + public static final String ROLE_NAME_SECURITY_OFFICER = "SecurityOfficer"; + public static final String ROLE_SECURITY_OFFICER = SimplePermissionReference.getPermissionReference(RecordsManagementModel.ASPECT_FILE_PLAN_COMPONENT, ROLE_NAME_SECURITY_OFFICER) + .toString(); + + public static final String ROLE_NAME_RECORDS_MANAGER = "RecordsManager"; + public static final String ROLE_RECORDS_MANAGER = SimplePermissionReference.getPermissionReference(RecordsManagementModel.ASPECT_FILE_PLAN_COMPONENT, ROLE_NAME_RECORDS_MANAGER) + .toString(); + + public static final String ROLE_NAME_ADMINISTRATOR = "Administrator"; + public static final String ROLE_ADMINISTRATOR = SimplePermissionReference.getPermissionReference(RecordsManagementModel.ASPECT_FILE_PLAN_COMPONENT, ROLE_NAME_ADMINISTRATOR).toString(); + + // Capability permissions + + public static final String DECLARE_RECORDS = "DeclareRecords"; + + public static final String VIEW_RECORDS = "ViewRecords"; + + public static final String CREATE_MODIFY_DESTROY_FOLDERS = "CreateModifyDestroyFolders"; + + public static final String EDIT_RECORD_METADATA = "EditRecordMetadata"; + + public static final String EDIT_NON_RECORD_METADATA = "EditNonRecordMetadata"; + + public static final String ADD_MODIFY_EVENT_DATES = "AddModifyEventDates"; + + public static final String CLOSE_FOLDERS = "CloseFolders"; + + public static final String DECLARE_RECORDS_IN_CLOSED_FOLDERS = "DeclareRecordsInClosedFolders"; + + public static final String RE_OPEN_FOLDERS = "ReOpenFolders"; + + public static final String CYCLE_VITAL_RECORDS = "CycleVitalRecords"; + + public static final String PLANNING_REVIEW_CYCLES = "PlanningReviewCycles"; + + public static final String UPDATE_TRIGGER_DATES = "UpdateTriggerDates"; + + public static final String CREATE_MODIFY_DESTROY_EVENTS = "CreateModifyDestroyEvents"; + + public static final String MANAGE_ACCESS_RIGHTS = "ManageAccessRights"; + + public static final String MOVE_RECORDS = "MoveRecords"; + + public static final String CHANGE_OR_DELETE_REFERENCES = "ChangeOrDeleteReferences"; + + public static final String DELETE_LINKS = "DeleteLinks"; + + public static final String EDIT_DECLARED_RECORD_METADATA = "EditDeclaredRecordMetadata"; + + public static final String MANUALLY_CHANGE_DISPOSITION_DATES = "ManuallyChangeDispositionDates"; + + public static final String APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF = "ApproveRecordsScheduledForCutoff"; + + public static final String CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS = "CreateModifyRecordsInCutoffFolders"; + + public static final String EXTEND_RETENTION_PERIOD_OR_FREEZE = "ExtendRetentionPeriodOrFreeze"; + + public static final String UNFREEZE = "Unfreeze"; + + public static final String VIEW_UPDATE_REASONS_FOR_FREEZE = "ViewUpdateReasonsForFreeze"; + + public static final String DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION = "DestroyRecordsScheduledForDestruction"; + + public static final String DESTROY_RECORDS = "DestroyRecords"; + + public static final String UPDATE_VITAL_RECORD_CYCLE_INFORMATION = "UpdateVitalRecordCycleInformation"; + + public static final String UNDECLARE_RECORDS = "UndeclareRecords"; + + public static final String DECLARE_AUDIT_AS_RECORD = "DeclareAuditAsRecord"; + + public static final String DELETE_AUDIT = "DeleteAudit"; + + public static final String CREATE_MODIFY_DESTROY_TIMEFRAMES = "CreateModifyDestroyTimeframes"; + + public static final String AUTHORIZE_NOMINATED_TRANSFERS = "AuthorizeNominatedTransfers"; + + public static final String EDIT_SELECTION_LISTS = "EditSelectionLists"; + + public static final String AUTHORIZE_ALL_TRANSFERS = "AuthorizeAllTransfers"; + + public static final String CREATE_MODIFY_DESTROY_FILEPLAN_METADATA = "CreateModifyDestroyFileplanMetadata"; + + public static final String CREATE_AND_ASSOCIATE_SELECTION_LISTS = "CreateAndAssociateSelectionLists"; + + public static final String ATTACH_RULES_TO_METADATA_PROPERTIES = "AttachRulesToMetadataProperties"; + + public static final String CREATE_MODIFY_DESTROY_FILEPLAN_TYPES = "CreateModifyDestroyFileplanTypes"; + + public static final String CREATE_MODIFY_DESTROY_RECORD_TYPES = "CreateModifyDestroyRecordTypes"; + + public static final String MAKE_OPTIONAL_PARAMETERS_MANDATORY = "MakeOptionalParametersMandatory"; + + public static final String MAP_EMAIL_METADATA = "MapEmailMetadata"; + + public static final String DELETE_RECORDS = "DeleteRecords"; + + public static final String TRIGGER_AN_EVENT = "TriggerAnEvent"; + + public static final String CREATE_MODIFY_DESTROY_ROLES = "CreateModifyDestroyRoles"; + + public static final String CREATE_MODIFY_DESTROY_USERS_AND_GROUPS = "CreateModifyDestroyUsersAndGroups"; + + public static final String PASSWORD_CONTROL = "PasswordControl"; + + public static final String ENABLE_DISABLE_AUDIT_BY_TYPES = "EnableDisableAuditByTypes"; + + public static final String SELECT_AUDIT_METADATA = "SelectAuditMetadata"; + + public static final String DISPLAY_RIGHTS_REPORT = "DisplayRightsReport"; + + public static final String ACCESS_AUDIT = "AccessAudit"; + + public static final String EXPORT_AUDIT = "ExportAudit"; + + public static final String CREATE_MODIFY_DESTROY_REFERENCE_TYPES = "CreateModifyDestroyReferenceTypes"; + + public static final String UPDATE_CLASSIFICATION_DATES = "UpdateClassificationDates"; + + public static final String CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES = "CreateModifyDestroyClassificationGuides"; + + public static final String UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS = "UpgradeDowngradeAndDeclassifyRecords"; + + public static final String UPDATE_EXEMPTION_CATEGORIES = "UpdateExemptionCategories"; + + public static final String MAP_CLASSIFICATION_GUIDE_METADATA = "MapClassificationGuideMetadata"; + + public static final String MANAGE_ACCESS_CONTROLS = "ManageAccessControls"; +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/RMSecurityCommon.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/RMSecurityCommon.java new file mode 100644 index 0000000000..fd58ad0463 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/RMSecurityCommon.java @@ -0,0 +1,215 @@ +/* + * 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.module.org_alfresco_module_rm.capability; + +import net.sf.acegisecurity.vote.AccessDecisionVoter; + +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.caveat.RMCaveatConfigComponent; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.transaction.AlfrescoTransactionSupport; +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.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * @author Roy Wetherall + * @since 2.0 + */ +public class RMSecurityCommon +{ + protected int NOSET_VALUE = -100; + + private static Log logger = LogFactory.getLog(RMSecurityCommon.class); + + protected NodeService nodeService; + protected PermissionService permissionService; + protected RecordsManagementService rmService; + protected RMCaveatConfigComponent caveatConfigComponent; + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setPermissionService(PermissionService permissionService) + { + this.permissionService = permissionService; + } + + public void setRecordsManagementService(RecordsManagementService rmService) + { + this.rmService = rmService; + } + + public void setCaveatConfigComponent(RMCaveatConfigComponent caveatConfigComponent) + { + this.caveatConfigComponent = caveatConfigComponent; + } + + /** + * + * @param prefix + * @param nodeRef + * @param value + * @return + */ + protected int setTransactionCache(String prefix, NodeRef nodeRef, int value) + { + String user = AuthenticationUtil.getRunAsUser(); + AlfrescoTransactionSupport.bindResource(prefix + nodeRef.toString() + user, Integer.valueOf(value)); + return value; + } + + /** + * + * @param prefix + * @param nodeRef + * @return + */ + protected int getTransactionCache(String prefix, NodeRef nodeRef) + { + int result = NOSET_VALUE; + String user = AuthenticationUtil.getRunAsUser(); + Integer value = (Integer)AlfrescoTransactionSupport.getResource(prefix + nodeRef.toString() + user); + if (value != null) + { + result = value.intValue(); + } + return result; + } + + /** + * + * @param nodeRef + * @return + */ + public int checkRead(NodeRef nodeRef) + { + if (nodeRef != null) + { + // now we know the node - we can abstain for certain types and aspects (eg, rm) + return checkRead(nodeRef, false); + } + + return AccessDecisionVoter.ACCESS_ABSTAIN; + } + + /** + * + * @param nodeRef + * @param allowDMRead + * @return + */ + public int checkRead(NodeRef nodeRef, boolean allowDMRead) + { + if (nodeService.hasAspect(nodeRef, RecordsManagementModel.ASPECT_FILE_PLAN_COMPONENT)) + { + return checkRmRead(nodeRef); + } + else + { + if (allowDMRead) + { + // Check DM read for copy etc + // DM does not grant - it can only deny + if (permissionService.hasPermission(nodeRef, PermissionService.READ) == AccessStatus.DENIED) + { + if (logger.isDebugEnabled()) + { + logger.debug("\t\tPermission is denied"); + Thread.dumpStack(); + } + return AccessDecisionVoter.ACCESS_DENIED; + } + else + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + } + else + { + return AccessDecisionVoter.ACCESS_ABSTAIN; + } + } + } + + /** + * + * @param nodeRef + * @return + */ + public int checkRmRead(NodeRef nodeRef) + { + int result = getTransactionCache("checkRmRead", nodeRef); + if (result != NOSET_VALUE) + { + return result; + } + + // Get the file plan for the node + NodeRef filePlan = rmService.getFilePlan(nodeRef); + + // Admin role + if (permissionService.hasPermission(filePlan, RMPermissionModel.ROLE_ADMINISTRATOR) == AccessStatus.ALLOWED) + { + if (logger.isDebugEnabled()) + { + logger.debug("\t\tAdmin access"); + Thread.dumpStack(); + } + return setTransactionCache("checkRmRead", nodeRef, AccessDecisionVoter.ACCESS_GRANTED); + } + + if (permissionService.hasPermission(nodeRef, RMPermissionModel.READ_RECORDS) == AccessStatus.DENIED) + { + if (logger.isDebugEnabled()) + { + logger.debug("\t\tPermission is denied"); + Thread.dumpStack(); + } + return setTransactionCache("checkRmRead", nodeRef, AccessDecisionVoter.ACCESS_DENIED); + } + + if (permissionService.hasPermission(filePlan, RMPermissionModel.VIEW_RECORDS) == AccessStatus.DENIED) + { + if (logger.isDebugEnabled()) + { + logger.debug("\t\tPermission is denied"); + Thread.dumpStack(); + } + return setTransactionCache("checkRmRead", nodeRef, AccessDecisionVoter.ACCESS_DENIED); + } + + if (caveatConfigComponent.hasAccess(nodeRef)) + { + return setTransactionCache("checkRmRead", nodeRef, AccessDecisionVoter.ACCESS_GRANTED); + } + else + { + return setTransactionCache("checkRmRead", nodeRef, AccessDecisionVoter.ACCESS_DENIED); + } + + } + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/AbstractCapabilityCondition.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/AbstractCapabilityCondition.java new file mode 100644 index 0000000000..f09769a026 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/AbstractCapabilityCondition.java @@ -0,0 +1,85 @@ +/* + * 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.module.org_alfresco_module_rm.capability.declarative; + +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.security.PermissionService; +import org.springframework.beans.factory.BeanNameAware; + +/** + * Abstract capability condition. + * + * @author Roy Wetherall + */ +public abstract class AbstractCapabilityCondition implements CapabilityCondition, + BeanNameAware, + RecordsManagementModel +{ + /** Capability condition name */ + protected String name; + + /** Services */ + protected RecordsManagementService rmService; + protected PermissionService permissionService; + protected NodeService nodeService; + + /** + * @param rmService records management service + */ + public void setRecordsManagementService(RecordsManagementService rmService) + { + this.rmService = rmService; + } + + /** + * @param permissionService permission service + */ + public void setPermissionService(PermissionService permissionService) + { + this.permissionService = permissionService; + } + + /** + * @param nodeService node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.capability.declarative.CapabilityCondition#getName() + */ + @Override + public String getName() + { + return name; + } + + /** + * @see org.springframework.beans.factory.BeanNameAware#setBeanName(java.lang.String) + */ + @Override + public void setBeanName(String name) + { + this.name = name; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/CapabilityCondition.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/CapabilityCondition.java new file mode 100644 index 0000000000..08032bcd22 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/CapabilityCondition.java @@ -0,0 +1,28 @@ +/* + * 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.module.org_alfresco_module_rm.capability.declarative; + +import org.alfresco.service.cmr.repository.NodeRef; + +public interface CapabilityCondition +{ + String getName(); + + boolean evaluate(NodeRef nodeRef); +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/DeclarativeCapability.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/DeclarativeCapability.java new file mode 100644 index 0000000000..5b9927d1a5 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/DeclarativeCapability.java @@ -0,0 +1,289 @@ +/* + * 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.module.org_alfresco_module_rm.capability.declarative; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import net.sf.acegisecurity.vote.AccessDecisionVoter; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.module.org_alfresco_module_rm.FilePlanComponentKind; +import org.alfresco.module.org_alfresco_module_rm.capability.impl.AbstractCapability; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.AccessStatus; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; + +/** + * Declarative capability implementation. + * + * @author Roy Wetherall + */ +public class DeclarativeCapability extends AbstractCapability implements ApplicationContextAware +{ + /** Application Context */ + private ApplicationContext applicationContext; + + /** Required permissions */ + private List permissions; + + /** Map of conditions and expected evaluation result */ + private Map conditions; + + /** List of file plan component kinds one of which must be satisfied */ + private List kinds; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException + { + this.applicationContext = applicationContext; + } + + /** + * @param permissions permissions + */ + public void setPermissions(List permissions) + { + this.permissions = permissions; + } + + /** + * @param conditions conditions and expected values + */ + public void setConditions(Map conditions) + { + this.conditions = conditions; + } + + /** + * @return {@link Map} conditions and expected values + */ + public Map getConditions() + { + return conditions; + } + + /** + * @param kinds list of file plan component kinds that the + */ + public void setKinds(List kinds) + { + this.kinds = kinds; + } + + /** + * @return {@link List}<@link String> list of expected file plan component kinds + */ + public List getKinds() + { + return kinds; + } + + /** + * Helper @see #setPermissions(List) + * + * @param permission permission + */ + public void setPermission(String permission) + { + List permissions = new ArrayList(1); + permissions.add(permission); + this.permissions = permissions; + } + + /** + * Check the permissions passed. + * + * @param nodeRef node reference + * @return boolean true if the permissions are present, false otherwise + */ + protected boolean checkPermissionsImpl(NodeRef nodeRef, String ... permissions) + { + boolean result = true; + NodeRef filePlan = rmService.getFilePlan(nodeRef); + + for (String permission : permissions) + { + if (permissionService.hasPermission(filePlan, permission) != AccessStatus.ALLOWED) + { + result = false; + break; + } + } + + return result; + } + + /** + * Checks the permissions required for the capability. + * + * @param nodeRef + * @return + */ + protected boolean checkPermissions(NodeRef nodeRef) + { + boolean result = true; + if (permissions != null && permissions.isEmpty() == false) + { + result = checkPermissionsImpl(nodeRef, (String[])permissions.toArray(new String[permissions.size()])); + } + return result; + } + + /** + * Checks the passed conditions. + * + * @param nodeRef + * @return + */ + protected boolean checkConditions(NodeRef nodeRef, Map conditions) + { + boolean result = true; + if (conditions != null) + { + for (Map.Entry entry : conditions.entrySet()) + { + boolean expected = entry.getValue().booleanValue(); + String conditionName = entry.getKey(); + + CapabilityCondition condition = (CapabilityCondition)applicationContext.getBean(conditionName); + if (condition == null) + { + throw new AlfrescoRuntimeException("Capability condition " + conditionName + " does not exist. Check the configuration of the capability " + name + "."); + } + + boolean actual = condition.evaluate(nodeRef); + if (expected != actual) + { + result = false; + break; + } + } + + } + return result; + } + + /** + * Checks the set conditions. + * + * @param nodeRef node reference + * @return boolean true if conditions satisfied, false otherwise + */ + protected boolean checkConditions(NodeRef nodeRef) + { + return checkConditions(nodeRef, conditions); + } + + /** + * Checks that the node ref is of the expected kind + * + * @param nodeRef + * @return + */ + protected boolean checkKinds(NodeRef nodeRef) + { + boolean result = false; + + FilePlanComponentKind actualKind = rmService.getFilePlanComponentKind(nodeRef); + + if (actualKind != null) + { + if (kinds != null && kinds.isEmpty() == false) + { + // need to check the actual file plan kind is in the list specified + for (String kindString : kinds) + { + FilePlanComponentKind kind = FilePlanComponentKind.valueOf(kindString); + if (actualKind.equals(kind) == true) + { + result = true; + break; + } + } + } + else + { + // we don't have any specific kinds to check, so we pass since we have a file + // plan component in our hands + result = true; + } + } + + return result; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.capability.impl.AbstractCapability#hasPermissionImpl(org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + public int evaluate(NodeRef nodeRef) + { + int result = AccessDecisionVoter.ACCESS_ABSTAIN; + + // Check we are dealing with a file plan component + if (rmService.isFilePlanComponent(nodeRef) == true) + { + // Check the kind of the object, the permissions and the conditions + if (checkKinds(nodeRef) == true && checkPermissions(nodeRef) == true && checkConditions(nodeRef) == true) + { + // Opportunity for child implementations to extend + result = evaluateImpl(nodeRef); + } + else + { + result = AccessDecisionVoter.ACCESS_DENIED; + } + } + + // Last chance for child implementations to veto/change the result + result = onEvaluate(nodeRef, result); + + return result; + } + + /** + * Default implementation. Given extending classes a hook point for further checks. + * + * @param nodeRef node reference + * @return + */ + protected int evaluateImpl(NodeRef nodeRef) + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + + /** + * Default implementation. + * + * Called before evaluate completes. The result returned overwrites the already discovered result. + * Provides a hook point for child implementations that wish to veto the result. + * + * @param nodeRef + * @param result + * @return + */ + protected int onEvaluate(NodeRef nodeRef, int result) + { + return result; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/ClosedCapabilityCondition.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/ClosedCapabilityCondition.java new file mode 100644 index 0000000000..55d1b2756e --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/ClosedCapabilityCondition.java @@ -0,0 +1,60 @@ +/* + * 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.module.org_alfresco_module_rm.capability.declarative.condition; + +import java.util.List; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.capability.declarative.AbstractCapabilityCondition; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.RegexQNamePattern; + +/** + * @author Roy Wetherall + */ +public class ClosedCapabilityCondition extends AbstractCapabilityCondition +{ + /** + * @see org.alfresco.module.org_alfresco_module_rm.capability.declarative.CapabilityCondition#evaluate(org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + public boolean evaluate(NodeRef nodeRef) + { + boolean result = false; + if (rmService.isRecordFolder(nodeRef) == true) + { + result = rmService.isRecordFolderClosed(nodeRef); + } + else if (rmService.isRecord(nodeRef) == true) + { + List assocs = nodeService.getParentAssocs(nodeRef, ContentModel.ASSOC_CONTAINS, RegexQNamePattern.MATCH_ALL); + for (ChildAssociationRef assoc : assocs) + { + if (rmService.isRecordFolderClosed(assoc.getParentRef()) == true) + { + result = true; + break; + } + } + } + return result; + } + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/CutoffCapabilityCondition.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/CutoffCapabilityCondition.java new file mode 100644 index 0000000000..37eb259e98 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/CutoffCapabilityCondition.java @@ -0,0 +1,38 @@ +/* + * 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.module.org_alfresco_module_rm.capability.declarative.condition; + +import org.alfresco.module.org_alfresco_module_rm.capability.declarative.AbstractCapabilityCondition; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * @author Roy Wetherall + */ +public class CutoffCapabilityCondition extends AbstractCapabilityCondition +{ + /** + * @see org.alfresco.module.org_alfresco_module_rm.capability.declarative.CapabilityCondition#evaluate(org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + public boolean evaluate(NodeRef nodeRef) + { + return rmService.isCutoff(nodeRef); + } + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/DeclaredCapabilityCondition.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/DeclaredCapabilityCondition.java new file mode 100644 index 0000000000..76cc017214 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/DeclaredCapabilityCondition.java @@ -0,0 +1,34 @@ +/* + * 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.module.org_alfresco_module_rm.capability.declarative.condition; + +import org.alfresco.module.org_alfresco_module_rm.capability.declarative.AbstractCapabilityCondition; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * @author Roy Wetherall + */ +public class DeclaredCapabilityCondition extends AbstractCapabilityCondition +{ + @Override + public boolean evaluate(NodeRef nodeRef) + { + return rmService.isRecordDeclared(nodeRef); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/DestroyedCapabilityCondition.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/DestroyedCapabilityCondition.java new file mode 100644 index 0000000000..f91fa9a47c --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/DestroyedCapabilityCondition.java @@ -0,0 +1,36 @@ +/* + * 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.module.org_alfresco_module_rm.capability.declarative.condition; + +import org.alfresco.module.org_alfresco_module_rm.capability.declarative.AbstractCapabilityCondition; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Destroyed capability condition. + * + * @author Roy Wetherall + */ +public class DestroyedCapabilityCondition extends AbstractCapabilityCondition +{ + @Override + public boolean evaluate(NodeRef nodeRef) + { + return nodeService.hasAspect(nodeRef, ASPECT_GHOSTED); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/FileableCapabilityCondition.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/FileableCapabilityCondition.java new file mode 100644 index 0000000000..1b52f2d838 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/FileableCapabilityCondition.java @@ -0,0 +1,57 @@ +/* + * 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.module.org_alfresco_module_rm.capability.declarative.condition; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.capability.declarative.AbstractCapabilityCondition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + +/** + * Fileable capability condition. Indicates whether a node is 'fileable', namely if it extends cm:content + * or extends rma:nonElectronicDocument + * + * @author Roy Wetherall + */ +public class FileableCapabilityCondition extends AbstractCapabilityCondition +{ + /** Dictionary service */ + private DictionaryService dictionaryService; + + /** + * @param dictionaryService dictionary service + */ + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.capability.declarative.CapabilityCondition#evaluate(org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + public boolean evaluate(NodeRef nodeRef) + { + QName type = nodeService.getType(nodeRef); + return (dictionaryService.isSubClass(type, ContentModel.TYPE_CONTENT) || + dictionaryService.isSubClass(type, TYPE_NON_ELECTRONIC_DOCUMENT)); + } + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/FillingCapabilityCondition.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/FillingCapabilityCondition.java new file mode 100644 index 0000000000..61366a9629 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/FillingCapabilityCondition.java @@ -0,0 +1,117 @@ +/* + * 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.module.org_alfresco_module_rm.capability.declarative.condition; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.capability.RMPermissionModel; +import org.alfresco.module.org_alfresco_module_rm.capability.declarative.AbstractCapabilityCondition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.namespace.QName; + +/** + * Filling capability condition. + * + * @author Roy Wetherall + */ +public class FillingCapabilityCondition extends AbstractCapabilityCondition +{ + /** Dictionary service */ + private DictionaryService dictionaryService; + + /** + * @param dictionaryService dictionary service + */ + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.capability.declarative.CapabilityCondition#evaluate(org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + public boolean evaluate(NodeRef nodeRef) + { + boolean result = false; + + NodeRef filePlan = rmService.getFilePlan(nodeRef); + + if (permissionService.hasPermission(filePlan, RMPermissionModel.ROLE_ADMINISTRATOR) == AccessStatus.ALLOWED) + { + result = true; + } + else + { + QName nodeType = nodeService.getType(nodeRef); + if (rmService.isRecord(nodeRef) == true || + dictionaryService.isSubClass(nodeType, ContentModel.TYPE_CONTENT) == true) + { + // Multifiling - if you have filing rights to any of the folders in which the record resides + // then you have filing rights. + for (ChildAssociationRef car : nodeService.getParentAssocs(nodeRef)) + { + if (car != null) + { + if (permissionService.hasPermission(car.getParentRef(), RMPermissionModel.FILE_RECORDS) == AccessStatus.ALLOWED) + { + result = true; + break; + } + } + } + } + else if (rmService.isRecordFolder(nodeRef) == true) + { + if (permissionService.hasPermission(nodeRef, RMPermissionModel.FILE_RECORDS) != AccessStatus.DENIED) + { + result = true; + } + } + else if (rmService.isRecordCategory(nodeRef) == true) + { + if (permissionService.hasPermission(nodeRef, RMPermissionModel.FILE_RECORDS) != AccessStatus.DENIED) + { + result = true; + } + else if (permissionService.hasPermission(filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS) != AccessStatus.DENIED) + { + result = true; + } + } + // else other file plan component + else + { + if (permissionService.hasPermission(nodeRef, RMPermissionModel.FILE_RECORDS) != AccessStatus.DENIED) + { + result = true; + } + else if (permissionService.hasPermission(filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA) != AccessStatus.DENIED) + { + result = true; + } + } + + } + + return result; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/FrozenCapabilityCondition.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/FrozenCapabilityCondition.java new file mode 100644 index 0000000000..11d74e4d63 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/FrozenCapabilityCondition.java @@ -0,0 +1,47 @@ +/* + * 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.module.org_alfresco_module_rm.capability.declarative.condition; + +import org.alfresco.module.org_alfresco_module_rm.capability.declarative.AbstractCapabilityCondition; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * @author Roy Wetherall + */ +public class FrozenCapabilityCondition extends AbstractCapabilityCondition +{ +private boolean checkChildren = false; + + public void setCheckChildren(boolean checkChildren) + { + this.checkChildren = checkChildren; + } + + @Override + public boolean evaluate(NodeRef nodeRef) + { + boolean result = rmService.isFrozen(nodeRef); + if (result == false && checkChildren == true) + { + result = rmService.hasFrozenChildren(nodeRef); + } + return result; + } + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/FrozenOrHoldCondition.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/FrozenOrHoldCondition.java new file mode 100644 index 0000000000..025f63d9b4 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/FrozenOrHoldCondition.java @@ -0,0 +1,43 @@ +/* + * 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.module.org_alfresco_module_rm.capability.declarative.condition; + +import org.alfresco.module.org_alfresco_module_rm.FilePlanComponentKind; +import org.alfresco.module.org_alfresco_module_rm.capability.declarative.AbstractCapabilityCondition; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Indicates whether the node is either frozen or is a hold object + * + * @author Roy Wetherall + */ +public class FrozenOrHoldCondition extends AbstractCapabilityCondition +{ + /** + * @see org.alfresco.module.org_alfresco_module_rm.capability.declarative.CapabilityCondition#evaluate(org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + public boolean evaluate(NodeRef nodeRef) + { + FilePlanComponentKind kind = rmService.getFilePlanComponentKind(nodeRef); + return (rmService.isFrozen(nodeRef) || + (kind != null && kind.equals(FilePlanComponentKind.HOLD))); + } + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/HasEventsCapabilityCondition.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/HasEventsCapabilityCondition.java new file mode 100644 index 0000000000..c2270ddced --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/HasEventsCapabilityCondition.java @@ -0,0 +1,63 @@ +/* + * 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.module.org_alfresco_module_rm.capability.declarative.condition; + +import org.alfresco.module.org_alfresco_module_rm.capability.declarative.AbstractCapabilityCondition; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionAction; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Indicates whether a scheduled record or folder has events or not. + * + * @author Roy Wetherall + */ +public class HasEventsCapabilityCondition extends AbstractCapabilityCondition +{ + /** Disposition service */ + private DispositionService dispositionService; + + /** + * @param dispositionService disposition service + */ + public void setDispositionService(DispositionService dispositionService) + { + this.dispositionService = dispositionService; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.capability.declarative.CapabilityCondition#evaluate(org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + public boolean evaluate(NodeRef nodeRef) + { + boolean result = false; + + if (dispositionService.isDisposableItem(nodeRef) == true) + { + DispositionAction da = dispositionService.getNextDispositionAction(nodeRef); + if (da != null) + { + result = (da.getEventCompletionDetails().isEmpty() == false); + } + } + + return result; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/IsScheduledCapabilityCondition.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/IsScheduledCapabilityCondition.java new file mode 100644 index 0000000000..1f9af16c9d --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/IsScheduledCapabilityCondition.java @@ -0,0 +1,77 @@ +/* + * 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.module.org_alfresco_module_rm.capability.declarative.condition; + +import org.alfresco.module.org_alfresco_module_rm.capability.declarative.AbstractCapabilityCondition; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionAction; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Indicates whether the given disposition action 'may' be scheduled in the future + * + * @author Roy Wetherall + */ +public class IsScheduledCapabilityCondition extends AbstractCapabilityCondition +{ + /** Disposition action */ + private String dispositionAction; + + /** Disposition service */ + private DispositionService dispositionService; + + /** + * @param dispositionAction disposition action + */ + public void setDispositionAction(String dispositionAction) + { + this.dispositionAction = dispositionAction; + } + + /** + * @param dispositionService disposition service + */ + public void setDispositionService(DispositionService dispositionService) + { + this.dispositionService = dispositionService; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.capability.declarative.CapabilityCondition#evaluate(org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + public boolean evaluate(NodeRef nodeRef) + { + boolean result = false; + + DispositionAction nextDispositionAction = dispositionService.getNextDispositionAction(nodeRef); + if (nextDispositionAction != null) + { + // Get the disposition actions name + String actionName = nextDispositionAction.getName(); + if (actionName.equals(dispositionAction) == true && + dispositionService.isNextDispositionActionEligible(nodeRef) == true) + { + result = true; + } + } + + return result; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/MayBeScheduledCapabilityCondition.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/MayBeScheduledCapabilityCondition.java new file mode 100644 index 0000000000..a0f7c09ba2 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/MayBeScheduledCapabilityCondition.java @@ -0,0 +1,105 @@ +/* + * 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.module.org_alfresco_module_rm.capability.declarative.condition; + +import org.alfresco.module.org_alfresco_module_rm.capability.declarative.AbstractCapabilityCondition; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionActionDefinition; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Indicates whether the given disposition action 'may' be scheduled in the future + * + * @author Roy Wetherall + */ +public class MayBeScheduledCapabilityCondition extends AbstractCapabilityCondition +{ + /** Disposition action */ + private String dispositionAction; + + /** Disposition service */ + private DispositionService dispositionService; + + /** + * @param dispositionAction disposition action + */ + public void setDispositionAction(String dispositionAction) + { + this.dispositionAction = dispositionAction; + } + + /** + * @param dispositionService disposition service + */ + public void setDispositionService(DispositionService dispositionService) + { + this.dispositionService = dispositionService; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.capability.declarative.CapabilityCondition#evaluate(org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + public boolean evaluate(NodeRef nodeRef) + { + boolean result = false; + + DispositionSchedule dispositionSchedule = dispositionService.getDispositionSchedule(nodeRef); + if (dispositionSchedule != null) + { + if (checkDispositionLevel(nodeRef, dispositionSchedule) == true) + { + for (DispositionActionDefinition dispositionActionDefinition : dispositionSchedule.getDispositionActionDefinitions()) + { + if (dispositionActionDefinition.getName().equals(dispositionAction) == true) + { + result = true; + break; + } + } + } + + } + return result; + } + + /** + * Checks the disposition level + * + * @param nodeRef + * @param dispositionSchedule + * @return + */ + private boolean checkDispositionLevel(NodeRef nodeRef, DispositionSchedule dispositionSchedule) + { + boolean result = false; + boolean isRecordLevelDisposition = dispositionSchedule.isRecordLevelDisposition(); + if (rmService.isRecord(nodeRef) == true && isRecordLevelDisposition == true) + { + result = true; + } + else if (rmService.isRecordFolder(nodeRef) == true && isRecordLevelDisposition == false) + + { + result = true; + } + return result; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/TransferredCapabilityCondition.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/TransferredCapabilityCondition.java new file mode 100644 index 0000000000..39cdc3ff24 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/TransferredCapabilityCondition.java @@ -0,0 +1,35 @@ +/* + * 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.module.org_alfresco_module_rm.capability.declarative.condition; + +import org.alfresco.module.org_alfresco_module_rm.capability.declarative.AbstractCapabilityCondition; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * @author Roy Wetherall + */ +public class TransferredCapabilityCondition extends AbstractCapabilityCondition +{ + @Override + public boolean evaluate(NodeRef nodeRef) + { + return nodeService.hasAspect(nodeRef, RecordsManagementModel.ASPECT_TRANSFERRED); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/VitalRecordOrFolderCapabilityCondition.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/VitalRecordOrFolderCapabilityCondition.java new file mode 100644 index 0000000000..3160caf29a --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/VitalRecordOrFolderCapabilityCondition.java @@ -0,0 +1,52 @@ +/* + * 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.module.org_alfresco_module_rm.capability.declarative.condition; + +import org.alfresco.module.org_alfresco_module_rm.capability.declarative.AbstractCapabilityCondition; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * @author Roy Wetherall + */ +public class VitalRecordOrFolderCapabilityCondition extends AbstractCapabilityCondition +{ + @Override + public boolean evaluate(NodeRef nodeRef) + { + boolean result = false; + + if (rmService.isRecord(nodeRef) == true) + { + // Check the record for the vital record aspect + result = nodeService.hasAspect(nodeRef, RecordsManagementModel.ASPECT_VITAL_RECORD); + } + else if (rmService.isRecordFolder(nodeRef) == true) + { + // Check the folder for the vital record indicator + Boolean value = (Boolean)nodeService.getProperty(nodeRef, RecordsManagementModel.PROP_VITAL_RECORD_INDICATOR); + if (value != null) + { + result = value.booleanValue(); + } + } + + return result; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/CreateCapability.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/CreateCapability.java new file mode 100644 index 0000000000..b19d558a4e --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/CreateCapability.java @@ -0,0 +1,160 @@ +/* + * 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.module.org_alfresco_module_rm.capability.group; + +import java.util.HashMap; +import java.util.Map; + +import net.sf.acegisecurity.vote.AccessDecisionVoter; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.capability.RMPermissionModel; +import org.alfresco.module.org_alfresco_module_rm.capability.declarative.DeclarativeCapability; +import org.alfresco.module.org_alfresco_module_rm.capability.impl.ChangeOrDeleteReferencesCapability; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.namespace.QName; + +/** + * Create group capability implementation + * + * @author Andy Hind + */ +public class CreateCapability extends DeclarativeCapability +{ + /** + * @see org.alfresco.module.org_alfresco_module_rm.capability.Capability#evaluate(org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + public int evaluate(NodeRef nodeRef) + { + return evaluate(nodeRef, null, null, null); + } + + /** + * + * @param destination + * @param linkee + * @param type + * @param assocType + * @return + */ + public int evaluate(NodeRef destination, NodeRef linkee, QName type, QName assocType) + { + if (linkee != null) + { + int state = checkRead(linkee, true); + if (state != AccessDecisionVoter.ACCESS_GRANTED) + { + return AccessDecisionVoter.ACCESS_DENIED; + } + } + if (rmService.isFilePlanComponent(destination)) + { + if ((assocType == null) || assocType.equals(ContentModel.ASSOC_CONTAINS) == false) + { + if(linkee == null) + { + if(rmService.isRecord(destination) && rmService.isRecordDeclared(destination) == false) + { + if (permissionService.hasPermission(destination, RMPermissionModel.FILE_RECORDS) == AccessStatus.ALLOWED) + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + } + } + else + { + if(rmService.isRecord(linkee) && rmService.isRecord(destination) && rmService.isRecordDeclared(destination) == false) + { + if (permissionService.hasPermission(destination, RMPermissionModel.FILE_RECORDS) == AccessStatus.ALLOWED) + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + } + } + + } + + // Build the conditions map + Map conditions = new HashMap(5); + conditions.put("capabilityCondition.filling", Boolean.TRUE); + conditions.put("capabilityCondition.frozen", Boolean.FALSE); + conditions.put("capabilityCondition.closed", Boolean.FALSE); + conditions.put("capabilityCondition.cutoff", Boolean.FALSE); + + if (checkConditions(destination, conditions) == true) + { + if (rmService.isRecordFolder(destination)) + { + if (permissionService.hasPermission(destination, RMPermissionModel.FILE_RECORDS) == AccessStatus.ALLOWED) + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + } + } + + conditions.put("capabilityCondition.closed", Boolean.TRUE); + if (checkConditions(destination, conditions) == true) + { + if (rmService.isRecordFolder(destination)) + { + if (permissionService.hasPermission(rmService.getFilePlan(destination), RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS) == AccessStatus.ALLOWED) + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + } + } + + conditions.remove("capabilityCondition.closed"); + conditions.put("capabilityCondition.cutoff", Boolean.TRUE); + if (checkConditions(destination, conditions) == true) + { + if (rmService.isRecordFolder(destination)) + { + if (permissionService.hasPermission(rmService.getFilePlan(destination), RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS) == AccessStatus.ALLOWED) + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + } + } + } + if (capabilityService.getCapability(RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS).evaluate(destination) == AccessDecisionVoter.ACCESS_GRANTED) + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + if (capabilityService.getCapability(RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS).evaluate(destination) == AccessDecisionVoter.ACCESS_GRANTED) + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + if (capabilityService.getCapability(RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS).evaluate(destination) == AccessDecisionVoter.ACCESS_GRANTED) + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + if (capabilityService.getCapability(RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA).evaluate(destination) == AccessDecisionVoter.ACCESS_GRANTED) + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + if (((ChangeOrDeleteReferencesCapability)capabilityService.getCapability(RMPermissionModel.CHANGE_OR_DELETE_REFERENCES)).evaluate(destination, linkee) == AccessDecisionVoter.ACCESS_GRANTED) + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + return AccessDecisionVoter.ACCESS_DENIED; + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/DeclareCapability.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/DeclareCapability.java new file mode 100644 index 0000000000..9e696f855a --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/DeclareCapability.java @@ -0,0 +1,54 @@ +/* + * 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.module.org_alfresco_module_rm.capability.group; + +import net.sf.acegisecurity.vote.AccessDecisionVoter; + +import org.alfresco.module.org_alfresco_module_rm.capability.Capability; +import org.alfresco.module.org_alfresco_module_rm.capability.RMPermissionModel; +import org.alfresco.module.org_alfresco_module_rm.capability.impl.AbstractCapability; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Composite Declare capability + * + * @author andyh + */ +public class DeclareCapability extends AbstractCapability +{ + /* + * @see org.alfresco.module.org_alfresco_module_rm.capability.Capability#evaluate(org.alfresco.service.cmr.repository.NodeRef) + */ + public int evaluate(NodeRef declaree) + { + Capability recordsCapability = capabilityService.getCapability(RMPermissionModel.DECLARE_RECORDS); + Capability inClosedCapability = capabilityService.getCapability(RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS); + + if (recordsCapability.hasPermissionRaw(declaree) == AccessDecisionVoter.ACCESS_GRANTED) + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + if (inClosedCapability.hasPermissionRaw(declaree) == AccessDecisionVoter.ACCESS_GRANTED) + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + return AccessDecisionVoter.ACCESS_DENIED; + } + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/DeleteCapability.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/DeleteCapability.java new file mode 100644 index 0000000000..2af89a535a --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/DeleteCapability.java @@ -0,0 +1,67 @@ +/* + * 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.module.org_alfresco_module_rm.capability.group; + +import net.sf.acegisecurity.vote.AccessDecisionVoter; + +import org.alfresco.module.org_alfresco_module_rm.capability.Capability; +import org.alfresco.module.org_alfresco_module_rm.capability.RMPermissionModel; +import org.alfresco.module.org_alfresco_module_rm.capability.impl.AbstractCapability; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * @author andyh + */ +public class DeleteCapability extends AbstractCapability +{ + /** + * @see org.alfresco.module.org_alfresco_module_rm.capability.Capability#evaluate(org.alfresco.service.cmr.repository.NodeRef) + */ + public int evaluate(NodeRef deletee) + { + Capability schedRec = capabilityService.getCapability(RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION); + Capability destroy = capabilityService.getCapability(RMPermissionModel.DESTROY_RECORDS); + Capability delete = capabilityService.getCapability(RMPermissionModel.DELETE_RECORDS); + Capability desfileplan = capabilityService.getCapability(RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA); + Capability desfolder = capabilityService.getCapability(RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS); + + if (schedRec.evaluate(deletee) == AccessDecisionVoter.ACCESS_GRANTED) + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + if (destroy.evaluate(deletee) == AccessDecisionVoter.ACCESS_GRANTED) + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + if (delete.evaluate(deletee) == AccessDecisionVoter.ACCESS_GRANTED) + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + if (desfileplan.evaluate(deletee) == AccessDecisionVoter.ACCESS_GRANTED) + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + if (desfolder.evaluate(deletee, null) == AccessDecisionVoter.ACCESS_GRANTED) + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + return AccessDecisionVoter.ACCESS_DENIED; + } + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/UpdateCapability.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/UpdateCapability.java new file mode 100644 index 0000000000..39160cd70b --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/UpdateCapability.java @@ -0,0 +1,95 @@ +/* + * 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.module.org_alfresco_module_rm.capability.group; + +import java.io.Serializable; +import java.util.Map; + +import net.sf.acegisecurity.vote.AccessDecisionVoter; + +import org.alfresco.module.org_alfresco_module_rm.capability.Capability; +import org.alfresco.module.org_alfresco_module_rm.capability.impl.AbstractCapability; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + +/** + * @author andyh + */ +public class UpdateCapability extends AbstractCapability +{ + /** + * @see org.alfresco.module.org_alfresco_module_rm.capability.Capability#evaluate(org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + public int evaluate(NodeRef nodeRef) + { + return evaluate(nodeRef, null, null); + } + + /** + * + * @param nodeRef + * @param aspectQName + * @param properties + * @return + */ + public int evaluate(NodeRef nodeRef, QName aspectQName, Map properties) + { + if ((aspectQName != null) && (voter.isProtectedAspect(nodeRef, aspectQName))) + { + return AccessDecisionVoter.ACCESS_DENIED; + } + if ((properties != null) && (voter.includesProtectedPropertyChange(nodeRef, properties))) + { + return AccessDecisionVoter.ACCESS_DENIED; + } + + Capability destFolder = capabilityService.getCapability(CREATE_MODIFY_DESTROY_FOLDERS); + if (destFolder.evaluate(nodeRef) == AccessDecisionVoter.ACCESS_GRANTED) + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + + Capability fileplanMeta = capabilityService.getCapability(CREATE_MODIFY_DESTROY_FILEPLAN_METADATA); + if (fileplanMeta.evaluate(nodeRef) == AccessDecisionVoter.ACCESS_GRANTED) + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + + Capability recordMeta = capabilityService.getCapability(EDIT_DECLARED_RECORD_METADATA); + if (recordMeta.evaluate(nodeRef) == AccessDecisionVoter.ACCESS_GRANTED) + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + + Capability nonRecordMetadata = capabilityService.getCapability(EDIT_NON_RECORD_METADATA); + if (nonRecordMetadata.evaluate(nodeRef) == AccessDecisionVoter.ACCESS_GRANTED) + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + + Capability editRecordMetadata = capabilityService.getCapability(EDIT_RECORD_METADATA); + if (editRecordMetadata.evaluate(nodeRef) == AccessDecisionVoter.ACCESS_GRANTED) + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + + return AccessDecisionVoter.ACCESS_DENIED; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/UpdatePropertiesCapability.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/UpdatePropertiesCapability.java new file mode 100644 index 0000000000..cf36f4909a --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/UpdatePropertiesCapability.java @@ -0,0 +1,97 @@ +/* + * 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.module.org_alfresco_module_rm.capability.group; + +import java.io.Serializable; +import java.util.Map; + +import net.sf.acegisecurity.vote.AccessDecisionVoter; + +import org.alfresco.module.org_alfresco_module_rm.capability.Capability; +import org.alfresco.module.org_alfresco_module_rm.capability.impl.AbstractCapability; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + +/** + * @author andyh + */ +public class UpdatePropertiesCapability extends AbstractCapability +{ + /** + * @see org.alfresco.module.org_alfresco_module_rm.capability.Capability#evaluate(org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + public int evaluate(NodeRef nodeRef) + { + return evaluate(nodeRef, (Map)null); + } + + /** + * Evaluate cabability + * + * @param nodeRef + * @param properties + * @return + */ + public int evaluate(NodeRef nodeRef, Map properties) + { + if ((properties != null) && (voter.includesProtectedPropertyChange(nodeRef, properties))) + { + return AccessDecisionVoter.ACCESS_DENIED; + } + + Capability cap1 = capabilityService.getCapability(CREATE_MODIFY_DESTROY_FOLDERS); + if (cap1.evaluate(nodeRef) == AccessDecisionVoter.ACCESS_GRANTED) + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + + Capability cap2 = capabilityService.getCapability(CREATE_MODIFY_DESTROY_FILEPLAN_METADATA); + if (cap2.evaluate(nodeRef) == AccessDecisionVoter.ACCESS_GRANTED) + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + + Capability cap3 = capabilityService.getCapability(EDIT_DECLARED_RECORD_METADATA); + if (cap3.evaluate(nodeRef) == AccessDecisionVoter.ACCESS_GRANTED) + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + + Capability cap4 = capabilityService.getCapability(EDIT_NON_RECORD_METADATA); + if (cap4.evaluate(nodeRef) == AccessDecisionVoter.ACCESS_GRANTED) + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + + Capability cap5 = capabilityService.getCapability(EDIT_RECORD_METADATA); + if (cap5.evaluate(nodeRef) == AccessDecisionVoter.ACCESS_GRANTED) + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + + Capability cap6 = capabilityService.getCapability(CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS); + if (cap6.evaluate(nodeRef) == AccessDecisionVoter.ACCESS_GRANTED) + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + + return AccessDecisionVoter.ACCESS_DENIED; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/WriteContentCapability.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/WriteContentCapability.java new file mode 100644 index 0000000000..0da5b4a3fc --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/WriteContentCapability.java @@ -0,0 +1,55 @@ +/* + * 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.module.org_alfresco_module_rm.capability.group; + +import net.sf.acegisecurity.vote.AccessDecisionVoter; + +import org.alfresco.module.org_alfresco_module_rm.capability.RMPermissionModel; +import org.alfresco.module.org_alfresco_module_rm.capability.declarative.DeclarativeCapability; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.AccessStatus; + +/** + * @author andyh + */ +public class WriteContentCapability extends DeclarativeCapability +{ + /** + * @see org.alfresco.module.org_alfresco_module_rm.capability.Capability#evaluate(org.alfresco.service.cmr.repository.NodeRef) + */ + public int evaluate(NodeRef nodeRef) + { + int result = AccessDecisionVoter.ACCESS_ABSTAIN; + + if (rmService.isFilePlanComponent(nodeRef)) + { + result = AccessDecisionVoter.ACCESS_DENIED; + + if (checkKinds(nodeRef) == true && checkConditions(nodeRef) == true) + { + if (permissionService.hasPermission(nodeRef, RMPermissionModel.FILE_RECORDS) == AccessStatus.ALLOWED) + { + result = AccessDecisionVoter.ACCESS_GRANTED; + } + } + } + + return result; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/AbstractCapability.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/AbstractCapability.java new file mode 100644 index 0000000000..d195159d9d --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/AbstractCapability.java @@ -0,0 +1,278 @@ +/* + * 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.module.org_alfresco_module_rm.capability.impl; + +import java.util.ArrayList; +import java.util.List; + +import net.sf.acegisecurity.vote.AccessDecisionVoter; + +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction; +import org.alfresco.module.org_alfresco_module_rm.capability.Capability; +import org.alfresco.module.org_alfresco_module_rm.capability.CapabilityService; +import org.alfresco.module.org_alfresco_module_rm.capability.RMEntryVoter; +import org.alfresco.module.org_alfresco_module_rm.capability.RMPermissionModel; +import org.alfresco.module.org_alfresco_module_rm.capability.RMSecurityCommon; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.AccessStatus; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Abstract capability implementation. + * + * @author Andy Hind + * @author Roy Wetherall + */ +public abstract class AbstractCapability extends RMSecurityCommon + implements Capability, RecordsManagementModel, RMPermissionModel +{ + /** Logger */ + private static Log logger = LogFactory.getLog(AbstractCapability.class); + + /** RM entry voter */ + protected RMEntryVoter voter; + + /** Capability service */ + protected CapabilityService capabilityService; + + /** Capability name */ + protected String name; + + /** Indicates whether this is a group capability or not */ + protected boolean isGroupCapability = false; + + /** List of actions */ + protected List actions = new ArrayList(1); + + /** Action names */ + protected List actionNames = new ArrayList(1); + + /** + * @param voter RM entry voter + */ + public void setVoter(RMEntryVoter voter) + { + this.voter = voter; + } + + /** + * @param capabilityService capability service + */ + public void setCapabilityService(CapabilityService capabilityService) + { + this.capabilityService = capabilityService; + } + + /** + * Init method + */ + public void init() + { + capabilityService.registerCapability(this); + } + + /** + * Registers an action + * + * @param action + */ + public void registerAction(RecordsManagementAction action) + { + this.actions.add(action); + this.actionNames.add(action.getName()); + voter.addProtectedAspects(action.getProtectedAspects()); + voter.addProtectedProperties(action.getProtectedProperties()); + } + + /** + * @param name capability name + */ + public void setName(String name) + { + this.name = name; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.capability.Capability#getName() + */ + @Override + public String getName() + { + return name; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.capability.Capability#isGroupCapability() + */ + public boolean isGroupCapability() + { + return isGroupCapability; + } + + /** + * @param isGroupCapability indicates whether this is a group capability or not + */ + public void setGroupCapability(boolean isGroupCapability) + { + this.isGroupCapability = isGroupCapability; + } + + /** + * Translates the vote to an AccessStatus + * + * @param vote + * @return + */ + private AccessStatus translate(int vote) + { + switch (vote) + { + case AccessDecisionVoter.ACCESS_ABSTAIN: + return AccessStatus.UNDETERMINED; + case AccessDecisionVoter.ACCESS_GRANTED: + return AccessStatus.ALLOWED; + case AccessDecisionVoter.ACCESS_DENIED: + return AccessStatus.DENIED; + default: + return AccessStatus.UNDETERMINED; + } + } + + /** + * + * @param nodeRef + * @return + */ + public int checkActionConditionsIfPresent(NodeRef nodeRef) + { + String prefix = "checkActionConditionsIfPresent" + getName(); + int result = getTransactionCache(prefix, nodeRef); + if (result != NOSET_VALUE) + { + return result; + } + + if (actions.size() > 0) + { + for (RecordsManagementAction action : actions) + { + if (action.isExecutable(nodeRef, null)) + { + return setTransactionCache(prefix, nodeRef, AccessDecisionVoter.ACCESS_GRANTED); + } + } + return setTransactionCache(prefix, nodeRef, AccessDecisionVoter.ACCESS_DENIED); + } + else + { + return setTransactionCache(prefix, nodeRef, AccessDecisionVoter.ACCESS_GRANTED); + } + } + + public AccessStatus hasPermission(NodeRef nodeRef) + { + return translate(hasPermissionRaw(nodeRef)); + } + + public int hasPermissionRaw(NodeRef nodeRef) + { + String prefix = "hasPermissionRaw" + getName(); + int result = getTransactionCache(prefix, nodeRef); + if (result != NOSET_VALUE) + { + return result; + } + + if (checkRmRead(nodeRef) == AccessDecisionVoter.ACCESS_DENIED) + { + result = AccessDecisionVoter.ACCESS_DENIED; + } + else if (checkActionConditionsIfPresent(nodeRef) == AccessDecisionVoter.ACCESS_DENIED) + { + result = AccessDecisionVoter.ACCESS_DENIED; + } + else + { + result = hasPermissionImpl(nodeRef); + } + + return setTransactionCache(prefix, nodeRef, result); + } + + /** + * Default implementation. Override if different behaviour required. + * + * @param nodeRef + * @return + */ + protected int hasPermissionImpl(NodeRef nodeRef) + { + return evaluate(nodeRef); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.capability.Capability#evaluate(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.cmr.repository.NodeRef) + */ + public int evaluate(NodeRef source, NodeRef target) + { + return AccessDecisionVoter.ACCESS_ABSTAIN; + } + + public List getActionNames() + { + return actionNames; + } + + public List getActions() + { + return actions; + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + ((getName() == null) ? 0 : getName().hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + final AbstractCapability other = (AbstractCapability) obj; + if (getName() == null) + { + if (other.getName() != null) + return false; + } + else if (!getName().equals(other.getName())) + return false; + return true; + } + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/ChangeOrDeleteReferencesCapability.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/ChangeOrDeleteReferencesCapability.java new file mode 100644 index 0000000000..121cf3b263 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/ChangeOrDeleteReferencesCapability.java @@ -0,0 +1,81 @@ +/* + * 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.module.org_alfresco_module_rm.capability.impl; + +import net.sf.acegisecurity.vote.AccessDecisionVoter; + +import org.alfresco.module.org_alfresco_module_rm.capability.declarative.DeclarativeCapability; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Change or delete references capability + * + * @author Roy Wetherall + */ +public class ChangeOrDeleteReferencesCapability extends DeclarativeCapability +{ + /** + * @see org.alfresco.module.org_alfresco_module_rm.capability.declarative.DeclarativeCapability#evaluateImpl(org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + protected int evaluateImpl(NodeRef nodeRef) + { + // Can't be sure, because we don't have information about the target so we still abstain + return AccessDecisionVoter.ACCESS_ABSTAIN; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.capability.impl.AbstractCapability#evaluate(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.cmr.repository.NodeRef) + */ + public int evaluate(NodeRef source, NodeRef target) + { + if (rmService.isFilePlanComponent(source)) + { + if (target != null) + { + if (rmService.isFilePlanComponent(target) == true) + { + if (checkConditions(source) == true && checkConditions(target) == true) + { + if (checkPermissions(source) == true && checkPermissions(target) == true) + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + } + } + } + else + { + if (checkConditions(source) == true) + { + if (checkPermissions(source) == true) + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + } + } + + return AccessDecisionVoter.ACCESS_DENIED; + } + else + { + return AccessDecisionVoter.ACCESS_ABSTAIN; + } + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/DeleteLinksCapability.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/DeleteLinksCapability.java new file mode 100644 index 0000000000..20273a3e43 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/DeleteLinksCapability.java @@ -0,0 +1,64 @@ +/* + * 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.module.org_alfresco_module_rm.capability.impl; + +import net.sf.acegisecurity.vote.AccessDecisionVoter; + +import org.alfresco.module.org_alfresco_module_rm.capability.declarative.DeclarativeCapability; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Delete links capability. + * + * @author Roy Wetherall + */ +public class DeleteLinksCapability extends DeclarativeCapability +{ + /** + * @see org.alfresco.module.org_alfresco_module_rm.capability.Capability#evaluate(org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + public int evaluate(NodeRef nodeRef) + { + // no way to know ... + return AccessDecisionVoter.ACCESS_ABSTAIN; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.capability.impl.AbstractCapability#evaluate(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.cmr.repository.NodeRef) + */ + public int evaluate(NodeRef source, NodeRef target) + { + if (rmService.isFilePlan(source) == true && rmService.isFilePlan(target) == true) + { + if (checkConditions(source) == true && checkConditions(target) == true) + { + if (checkPermissions(source) == true && checkPermissions(target) == true) + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + } + return AccessDecisionVoter.ACCESS_DENIED; + } + else + { + return AccessDecisionVoter.ACCESS_ABSTAIN; + } + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/EditRecordMetadataCapability.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/EditRecordMetadataCapability.java new file mode 100644 index 0000000000..aaa806b263 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/EditRecordMetadataCapability.java @@ -0,0 +1,99 @@ +/* + * 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.module.org_alfresco_module_rm.capability.impl; + +import net.sf.acegisecurity.vote.AccessDecisionVoter; + +import org.alfresco.module.org_alfresco_module_rm.capability.declarative.DeclarativeCapability; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.OwnableService; + +/** + * Edit record metadata capability implementation. + * + * @author Roy Wetherall + */ +public class EditRecordMetadataCapability extends DeclarativeCapability +{ + /** Ownable service */ + private OwnableService ownableService; + + /** + * @param ownableService ownable service + */ + public void setOwnableService(OwnableService ownableService) + { + this.ownableService = ownableService; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.capability.declarative.DeclarativeCapability#evaluate(org.alfresco.service.cmr.repository.NodeRef) + */ + public int evaluate(final NodeRef nodeRef) + { + // The default state is not knowing anything + int result = AccessDecisionVoter.ACCESS_ABSTAIN; + + // Check we are dealing with a file plan component + if (rmService.isFilePlanComponent(nodeRef) == true) + { + // Now the default state is denied + result = AccessDecisionVoter.ACCESS_DENIED; + + // Check the kind of the object, the permissions and the conditions + if (checkKinds(nodeRef) == true && checkConditions(nodeRef) == true) + { + if (checkPermissions(nodeRef) == true) + { + result = AccessDecisionVoter.ACCESS_GRANTED; + } + else + { + result = AuthenticationUtil.runAs(new RunAsWork() + { + @Override + public Integer doWork() throws Exception + { + Integer result = Integer.valueOf(AccessDecisionVoter.ACCESS_DENIED); + + // Since we know this is undeclared if you are the owner then you should be able to + // edit the records meta-data (otherwise how can it be declared by the user?) + if (ownableService.hasOwner(nodeRef) == true) + { + String user = AuthenticationUtil.getFullyAuthenticatedUser(); + if (user != null && + ownableService.getOwner(nodeRef).equals(user) == true) + { + result = Integer.valueOf(AccessDecisionVoter.ACCESS_GRANTED); + } + } + + return result; + } + + }, AuthenticationUtil.getSystemUserName()).intValue(); + } + } + } + + return result; + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/FileRecordsCapability.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/FileRecordsCapability.java new file mode 100644 index 0000000000..e8ecaa6829 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/FileRecordsCapability.java @@ -0,0 +1,115 @@ +/* + * 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.module.org_alfresco_module_rm.capability.impl; + +import java.util.HashMap; +import java.util.Map; + +import net.sf.acegisecurity.vote.AccessDecisionVoter; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.capability.RMPermissionModel; +import org.alfresco.module.org_alfresco_module_rm.capability.declarative.DeclarativeCapability; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.namespace.QName; + +/** + * File records capability. + * + * @author andyh + */ +public class FileRecordsCapability extends DeclarativeCapability +{ + /** Dictionary service */ + private DictionaryService dictionaryService; + + /** + * @param dictionaryService dictionary service + */ + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.capability.declarative.DeclarativeCapability#evaluate(org.alfresco.service.cmr.repository.NodeRef) + */ + public int evaluate(NodeRef nodeRef) + { + if (rmService.isFilePlanComponent(nodeRef)) + { + // Build the conditions map + Map conditions = new HashMap(5); + conditions.put("capabilityCondition.filling", Boolean.TRUE); + conditions.put("capabilityCondition.frozen", Boolean.FALSE); + conditions.put("capabilityCondition.cutoff", Boolean.FALSE); + conditions.put("capabilityCondition.closed", Boolean.FALSE); + conditions.put("capabilityCondition.declared", Boolean.FALSE); + + if (isFileable(nodeRef) || (rmService.isRecord(nodeRef) && checkConditions(nodeRef, conditions) == true)) + { + if (permissionService.hasPermission(nodeRef, RMPermissionModel.FILE_RECORDS) == AccessStatus.ALLOWED) + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + } + + conditions.put("capabilityCondition.closed", Boolean.TRUE); + if (isFileable(nodeRef) || (rmService.isRecord(nodeRef) && checkConditions(nodeRef, conditions) == true)) + { + if (checkPermissionsImpl(nodeRef, DECLARE_RECORDS_IN_CLOSED_FOLDERS) == true) + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + } + + conditions.put("capabilityCondition.cutoff", Boolean.TRUE); + conditions.remove("capabilityCondition.closed"); + conditions.remove("capabilityCondition.declared"); + if (isFileable(nodeRef) || (rmService.isRecord(nodeRef) && checkConditions(nodeRef, conditions) == true)) + { + if (checkPermissionsImpl(nodeRef, CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS) == true) + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + } + + return AccessDecisionVoter.ACCESS_DENIED; + + } + else + { + return AccessDecisionVoter.ACCESS_ABSTAIN; + } + } + + /** + * Indicate whether a node if 'fileable' or not. + * + * @param nodeRef node reference + * @return boolean true if the node is filable, false otherwise + */ + public boolean isFileable(NodeRef nodeRef) + { + QName type = nodeService.getType(nodeRef); + return dictionaryService.isSubClass(type, ContentModel.TYPE_CONTENT); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/MoveRecordsCapability.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/MoveRecordsCapability.java new file mode 100644 index 0000000000..376172dcfc --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/MoveRecordsCapability.java @@ -0,0 +1,92 @@ +/* + * 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.module.org_alfresco_module_rm.capability.impl; + +import net.sf.acegisecurity.vote.AccessDecisionVoter; + +import org.alfresco.module.org_alfresco_module_rm.capability.declarative.DeclarativeCapability; +import org.alfresco.module.org_alfresco_module_rm.capability.group.CreateCapability; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.namespace.QName; + +public class MoveRecordsCapability extends DeclarativeCapability +{ + @Override + public int evaluate(NodeRef nodeRef) + { + // no way to know ... + return AccessDecisionVoter.ACCESS_ABSTAIN; + } + + public int evaluate(NodeRef movee, NodeRef destination) + { + int state = AccessDecisionVoter.ACCESS_ABSTAIN; + + if (rmService.isFilePlanComponent(destination)) + { + state = checkRead(movee, true); + if (state != AccessDecisionVoter.ACCESS_GRANTED) + { + return AccessDecisionVoter.ACCESS_DENIED; + } + + if (rmService.isFilePlanComponent(movee) == true) + { + state = capabilityService.getCapability("Delete").evaluate(movee); + } + else + { + if (checkPermissionsImpl(movee, PermissionService.DELETE) == true) + { + state = AccessDecisionVoter.ACCESS_GRANTED; + } + } + + if (state == AccessDecisionVoter.ACCESS_GRANTED) + { + QName type = nodeService.getType(movee); + // now we know the node - we can abstain for certain types and aspects (eg, rm) + CreateCapability createCapability = (CreateCapability)capabilityService.getCapability("Create"); + state = createCapability.evaluate(destination, movee, type, null); + + if (state == AccessDecisionVoter.ACCESS_GRANTED) + { + if (rmService.isFilePlanComponent(movee) == true) + { + if (checkPermissionsImpl(movee, MOVE_RECORDS) == true) + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + } + else + { + return AccessDecisionVoter.ACCESS_GRANTED; + } + } + } + + return AccessDecisionVoter.ACCESS_DENIED; + } + else + { + return AccessDecisionVoter.ACCESS_ABSTAIN; + } + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/ViewRecordsCapability.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/ViewRecordsCapability.java new file mode 100644 index 0000000000..e00aba7985 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/ViewRecordsCapability.java @@ -0,0 +1,43 @@ +/* + * 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.module.org_alfresco_module_rm.capability.impl; + +import net.sf.acegisecurity.vote.AccessDecisionVoter; + +import org.alfresco.module.org_alfresco_module_rm.capability.declarative.DeclarativeCapability; +import org.alfresco.service.cmr.repository.NodeRef; + +public final class ViewRecordsCapability extends DeclarativeCapability +{ + /** + * @see org.alfresco.module.org_alfresco_module_rm.capability.declarative.DeclarativeCapability#evaluate(org.alfresco.service.cmr.repository.NodeRef) + */ + public int evaluate(NodeRef nodeRef) + { + if (nodeRef != null) + { + if (rmService.isFilePlanComponent(nodeRef) == true) + { + return checkRmRead(nodeRef); + } + } + + return AccessDecisionVoter.ACCESS_ABSTAIN; + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/PivotUtil.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/PivotUtil.java new file mode 100644 index 0000000000..efd01a3935 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/PivotUtil.java @@ -0,0 +1,55 @@ +/* + * 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.module.org_alfresco_module_rm.caveat; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/* package scope */ class PivotUtil +{ + static Map> getPivot(Map> source) + { + + Map> pivot = new HashMap>(); + + for(String authority : source.keySet()) + { + Listvalues = source.get(authority); + for(String value : values) + { + if(pivot.containsKey(value)) + { + // already exists + List list = pivot.get(value); + list.add(authority); + } + else + { + // New value + List list = new ArrayList(); + list.add(authority); + pivot.put(value, list); + } + } + } + return pivot; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/RMCaveatConfigComponent.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/RMCaveatConfigComponent.java new file mode 100644 index 0000000000..610e172b39 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/RMCaveatConfigComponent.java @@ -0,0 +1,112 @@ +/* + * 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.module.org_alfresco_module_rm.caveat; + +import java.io.File; +import java.io.InputStream; +import java.util.List; +import java.util.Map; + +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + +public interface RMCaveatConfigComponent +{ + public void init(); + + /** + * Get allowed values for given caveat list (for current user) + * + * @param constraintName + * @return + */ + public List getRMAllowedValues(String constraintName); + + /** + * Get custom caveat models + * + * @return + */ + public List getRMCaveatModels(); + + /** + * Check whether access to 'record component' node is vetoed for current user due to caveat(s) + * + * @param nodeRef + * @return false, if caveat(s) veto access otherwise return true + */ + public boolean hasAccess(NodeRef nodeRef); + + /** + * Get RM constraint list + * + * @param listName the name of the RMConstraintList + */ + public RMConstraintInfo getRMConstraint(String listName); + + /** + * Add RM constraint + */ + public void addRMConstraint(String listName); + + /** + * Add RM constraint value for given authority + */ + public void addRMConstraintListValue(String listName, String authorityName, String value); + + /** + * Update RM constraint values for given authority + */ + public void updateRMConstraintListAuthority(String listName, String authorityName, Listvalues); + + /** + * Update RM constraint authorities for given value + */ + public void updateRMConstraintListValue(String listName, String valueName, Listauthorities); + + /** + * Remove RM constraint value (all authorities) + */ + public void removeRMConstraintListValue(String listName, String valueName); + + /** + * Remove RM constraint authority (all values) + */ + public void removeRMConstraintListAuthority(String listName, String authorityName); + + /** + * Delete RM Constraint + * + * @param listName the name of the RMConstraintList + */ + public void deleteRMConstraint(String listName); + + /** + * Get the details of a caveat list + * @param listName + * @return + */ + public Map> getListDetails(String listName); + + public NodeRef updateOrCreateCaveatConfig(File jsonFile); + + public NodeRef updateOrCreateCaveatConfig(String jsonString); + + public NodeRef updateOrCreateCaveatConfig(InputStream is); +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/RMCaveatConfigComponentImpl.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/RMCaveatConfigComponentImpl.java new file mode 100644 index 0000000000..09a94de97a --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/RMCaveatConfigComponentImpl.java @@ -0,0 +1,945 @@ +/* + * 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.module.org_alfresco_module_rm.caveat; + +import java.io.File; +import java.io.InputStream; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.caveat.RMListOfValuesConstraint.MatchLogic; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.repo.content.ContentServicePolicies; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.node.NodeServicePolicies; +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.repo.version.VersionModel; +import org.alfresco.service.cmr.dictionary.Constraint; +import org.alfresco.service.cmr.dictionary.ConstraintDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +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; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.security.AuthorityService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.JSONtoFmModel; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * RM Caveat Config component impl + * + * @author janv + */ +public class RMCaveatConfigComponentImpl implements ContentServicePolicies.OnContentUpdatePolicy, + NodeServicePolicies.BeforeDeleteNodePolicy, + NodeServicePolicies.OnCreateNodePolicy, + RMCaveatConfigComponent +{ + private static Log logger = LogFactory.getLog(RMCaveatConfigComponentImpl.class); + + private PolicyComponent policyComponent; + private ContentService contentService; + private DictionaryService dictionaryService; + private NamespaceService namespaceService; + private AuthorityService authorityService; + private PersonService personService; + private NodeService nodeService; + + // Default + private StoreRef storeRef = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "SpacesStore"); + + private List caveatAspectURINames = new ArrayList(0); + private List caveatAspectQNames = new ArrayList(0); + + private List caveatModelURINames = new ArrayList(0); + private List caveatModelQNames = new ArrayList(0); + + private static final String CAVEAT_CONFIG_NAME = "caveatConfig.json"; + + private static final QName DATATYPE_TEXT = DataTypeDefinition.TEXT; + + + /* + * Caveat Config + * first string is property name + * second string is authority name (user or group full name) + * third string is list of values of property + */ + + // TODO - convert to SimpleCache to be cluster-aware (for dynamic changes to caveat config across a cluster) + private Map>> caveatConfig = new ConcurrentHashMap>>(2); + + public void setPolicyComponent(PolicyComponent policyComponent) + { + this.policyComponent = policyComponent; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setContentService(ContentService contentService) + { + this.contentService = contentService; + } + + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + public void setAuthorityService(AuthorityService authorityService) + { + this.authorityService = authorityService; + } + + public void setPersonService(PersonService personService) + { + this.personService = personService; + } + + public void setStoreRef(String storeRef) + { + this.storeRef = new StoreRef(storeRef); + } + + public void setCaveatAspects(List caveatAspectNames) + { + this.caveatAspectURINames = caveatAspectNames; + } + + public void setCaveatModels(List caveatModelNames) + { + this.caveatModelURINames = caveatModelNames; + } + + /** + * Initialise behaviours and caveat config cache + */ + public void init() + { + // Register interest in the onContentUpdate policy + policyComponent.bindClassBehaviour( + ContentServicePolicies.OnContentUpdatePolicy.QNAME, + RecordsManagementModel.TYPE_CAVEAT_CONFIG, + new JavaBehaviour(this, "onContentUpdate")); + + // Register interest in the beforeDeleteNode policy + policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "beforeDeleteNode"), + RecordsManagementModel.TYPE_CAVEAT_CONFIG, + new JavaBehaviour(this, "beforeDeleteNode")); + + // Register interest in the onCreateNode policy + policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onCreateNode"), + RecordsManagementModel.TYPE_CAVEAT_CONFIG, + new JavaBehaviour(this, "onCreateNode")); + + if (caveatAspectURINames.size() > 0) + { + for (String caveatAspectURIName : caveatAspectURINames) + { + caveatAspectQNames.add(QName.createQName(caveatAspectURIName)); + } + + if (logger.isInfoEnabled()) + { + logger.info("Caveat aspects configured "+caveatAspectQNames); + } + } + else + { + logger.warn("No caveat aspects configured - caveats will not be applied"); + } + + if (caveatModelURINames.size() > 0) + { + for (String caveatModelURIName : caveatModelURINames) + { + caveatModelQNames.add(QName.createQName(caveatModelURIName)); + } + + if (logger.isInfoEnabled()) + { + logger.info("Caveat models configured "+caveatModelQNames); + } + } + else + { + logger.info("No caveat models configured - all models will be checked"); + } + + NodeRef caveatConfigNodeRef = getCaveatConfigNode(); + if (caveatConfigNodeRef != null) + { + validateAndReset(caveatConfigNodeRef); + } + } + + public void onContentUpdate(NodeRef nodeRef, boolean newContent) + { + if (logger.isInfoEnabled()) + { + logger.info("onContentUpdate: "+nodeRef+", "+newContent); + } + + validateAndReset(nodeRef); + } + + public void beforeDeleteNode(NodeRef nodeRef) + { + if (logger.isInfoEnabled()) + { + logger.info("beforeDeleteNode: "+nodeRef); + } + + validateAndReset(nodeRef); + } + + public void onCreateNode(ChildAssociationRef childAssocRef) + { + if (logger.isInfoEnabled()) + { + logger.info("onCreateNode: "+childAssocRef); + } + + validateAndReset(childAssocRef.getChildRef()); + } + + @SuppressWarnings("unchecked") + protected void validateAndReset(NodeRef nodeRef) + { + ContentReader cr = contentService.getReader(nodeRef, ContentModel.PROP_CONTENT); + if (cr != null) + { + // TODO - check who can change caveat config ! + // TODO - locking (or checkout/checkin) + + String caveatConfigData = cr.getContentString(); + if (caveatConfigData != null) + { + NodeRef existing = getCaveatConfigNode(); + if ((existing != null && (! existing.equals(nodeRef)))) + { + throw new AlfrescoRuntimeException("Cannot create more than one caveat config (existing="+existing+", new="+nodeRef+")"); + } + + try + { + if (logger.isTraceEnabled()) + { + logger.trace(caveatConfigData); + } + + Set models = new HashSet(1); + Set props = new HashSet(10); + Set expectedPrefixes = new HashSet(10); + + if (caveatModelQNames.size() > 0) + { + models.addAll(caveatModelQNames); + } + else + { + models.addAll(dictionaryService.getAllModels()); + } + + if (logger.isTraceEnabled()) + { + logger.trace("validateAndReset: models to check "+models); + } + + for (QName model : models) + { + props.addAll(dictionaryService.getProperties(model, DATATYPE_TEXT)); + expectedPrefixes.addAll(namespaceService.getPrefixes(model.getNamespaceURI())); + } + + if (props.size() == 0) + { + logger.warn("validateAndReset: no caveat properties found"); + } + else + { + if (logger.isTraceEnabled()) + { + logger.trace("validateAndReset: properties to check "+props); + } + } + + Map caveatConfigMap = JSONtoFmModel.convertJSONObjectToMap(caveatConfigData); + + for (Map.Entry conEntry : caveatConfigMap.entrySet()) + { + String conStr = conEntry.getKey(); + + QName conQName = QName.resolveToQName(namespaceService, conStr); + + // check prefix + String conPrefix = QName.splitPrefixedQName(conStr)[0]; + boolean prefixFound = false; + for (String expectedPrefix : expectedPrefixes) + { + if (conPrefix.equals(expectedPrefix)) + { + prefixFound = true; + } + } + + if (! prefixFound) + { + throw new AlfrescoRuntimeException("Unexpected prefix: "+ conPrefix + " (" + conStr +") expected one of "+expectedPrefixes+")"); + } + + Map> caveatMap = (Map>)conEntry.getValue(); + + List allowedValues = null; + boolean found = false; + + for (QName propertyName : props) + { + PropertyDefinition propDef = dictionaryService.getProperty(propertyName); + List conDefs = propDef.getConstraints(); + for (ConstraintDefinition conDef : conDefs) + { + final Constraint con = conDef.getConstraint(); + if (con instanceof RMListOfValuesConstraint) + { + String conName = ((RMListOfValuesConstraint)con).getShortName(); + if (conName.equals(conStr)) + { + // note: assumes only one caveat/LOV against a given property + allowedValues = AuthenticationUtil.runAs(new RunAsWork>() + { + public List doWork() + { + return ((RMListOfValuesConstraint)con).getAllowedValues(); + } + }, AuthenticationUtil.getSystemUserName()); + + found = true; + break; + } + } + } + } + + if (! found) + { + //throw new AlfrescoRuntimeException("Constraint does not exist (or is not used): "+conStr); + } + + if (allowedValues != null) + { + if (logger.isInfoEnabled()) + { + logger.info("Processing constraint: "+conQName); + } + + for (Map.Entry> caveatEntry : caveatMap.entrySet()) + { + String authorityName = caveatEntry.getKey(); + List caveatList = caveatEntry.getValue(); + + // validate authority (user or group) - note: groups are configured with fullname (ie. GROUP_xxx) + if ((! authorityService.authorityExists(authorityName) && ! personService.personExists(authorityName))) + { + // TODO - review warnings (& I18N) + String msg = "User/group does not exist: "+authorityName+" (constraint="+conStr+")"; + logger.warn(msg); + } + + // validate caveat list + for (String value : caveatList) + { + if (! allowedValues.contains(value)) + { + // TODO - review warnings (& add I18N) + String msg = "Invalid value in list: "+value+" (authority="+authorityName+", constraint="+conStr+")"; + logger.warn(msg); + } + } + } + } + } + + // Valid, so update + caveatConfig.clear(); + + for (Map.Entry conEntry : caveatConfigMap.entrySet()) + { + String conStr = conEntry.getKey(); + Map> caveatMap = (Map>)conEntry.getValue(); + + caveatConfig.put(conStr, caveatMap); + } + } + catch (JSONException e) + { + throw new AlfrescoRuntimeException("Invalid caveat config syntax: "+e); + } + } + } + } + + private NodeRef getCaveatConfigNode() + { + NodeRef rootNode = nodeService.getRootNode(storeRef); + return nodeService.getChildByName(rootNode, RecordsManagementModel.ASSOC_CAVEAT_CONFIG, CAVEAT_CONFIG_NAME); + } + + + public NodeRef updateOrCreateCaveatConfig(InputStream is) + { + NodeRef caveatConfig = getOrCreateCaveatConfig(); + + // Update the content + ContentWriter writer = this.contentService.getWriter(caveatConfig, ContentModel.PROP_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.setEncoding("UTF-8"); + writer.putContent(is); + + return caveatConfig; + } + + public NodeRef updateOrCreateCaveatConfig(File jsonFile) + { + NodeRef caveatConfig = getOrCreateCaveatConfig(); + + // Update the content + ContentWriter writer = this.contentService.getWriter(caveatConfig, ContentModel.PROP_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.setEncoding("UTF-8"); + writer.putContent(jsonFile); + + return caveatConfig; + } + + public NodeRef updateOrCreateCaveatConfig(String jsonString) + { + NodeRef caveatConfig = getOrCreateCaveatConfig(); + + // Update the content + ContentWriter writer = this.contentService.getWriter(caveatConfig, ContentModel.PROP_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.setEncoding("UTF-8"); + writer.putContent(jsonString); + + return caveatConfig; + } + + private NodeRef getOrCreateCaveatConfig() + { + NodeRef caveatConfig = getCaveatConfigNode(); + if (caveatConfig == null) + { + NodeRef rootNode = nodeService.getRootNode(storeRef); + nodeService.addAspect(rootNode, VersionModel.ASPECT_VERSION_STORE_ROOT, null); + + // Create caveat config + caveatConfig = nodeService.createNode(rootNode, + RecordsManagementModel.ASSOC_CAVEAT_CONFIG, + QName.createQName(RecordsManagementModel.RM_URI, CAVEAT_CONFIG_NAME), + RecordsManagementModel.TYPE_CAVEAT_CONFIG).getChildRef(); + + nodeService.setProperty(caveatConfig, ContentModel.PROP_NAME, CAVEAT_CONFIG_NAME); + } + + return caveatConfig; + } + + // Get list of all caveat qualified names + public Set getRMConstraintNames() + { + return caveatConfig.keySet(); + } + + // Get allowed values for given caveat (for current user) + public List getRMAllowedValues(String constraintName) + { + List allowedValues = new ArrayList(0); + + String userName = AuthenticationUtil.getRunAsUser(); + if (userName != null) + { + if (! (AuthenticationUtil.isMtEnabled() && AuthenticationUtil.isRunAsUserTheSystemUser())) + { + // note: userName and userGroupNames must not be null + Map> caveatConstraintDef = caveatConfig.get(constraintName); + Set userGroupFullNames = authorityService.getAuthoritiesForUser(userName); + allowedValues = getRMAllowedValues(userName, userGroupFullNames, constraintName); + } + } + + return allowedValues; + } + + private List getRMAllowedValues(String userName, Set userGroupFullNames, String constraintName) + { + SetallowedValues = new HashSet(); + + // note: userName and userGroupNames must not be null + Map> caveatConstraintDef = caveatConfig.get(constraintName); + + if (caveatConstraintDef != null) + { + List direct = caveatConstraintDef.get(userName); + if(direct != null) + { + allowedValues.addAll(direct); + } + + for (String group : userGroupFullNames) + { + List values = caveatConstraintDef.get(group); + if(values != null) + { + allowedValues.addAll(values); + } + } + } + + Listret = new ArrayList(); + ret.addAll(allowedValues); + return ret; + } + + /** + * Check whether access to 'record component' node is vetoed for current user due to caveat(s) + * + * @param nodeRef + * @return false, if caveat(s) veto access otherwise return true + */ + @SuppressWarnings("unchecked") + public boolean hasAccess(NodeRef nodeRef) + { + if ((! nodeService.exists(nodeRef)) || (caveatAspectQNames.size() == 0)) + { + return true; + } + + boolean found = false; + for (QName caveatAspectQName : caveatAspectQNames) + { + if (nodeService.hasAspect(nodeRef, caveatAspectQName)) + { + found = true; + break; + } + } + + if (! found) + { + // no caveat aspect + return true; + } + else + { + // check for caveats + String userName = AuthenticationUtil.getRunAsUser(); + if (userName != null) + { + // check all text properties + Map props = nodeService.getProperties(nodeRef); + for (Map.Entry entry : props.entrySet()) + { + QName propName = entry.getKey(); + PropertyDefinition propDef = dictionaryService.getProperty(propName); + + if ((propDef != null) && (propDef.getDataType().getName().equals(DATATYPE_TEXT))) + { + List conDefs = propDef.getConstraints(); + for (ConstraintDefinition conDef : conDefs) + { + Constraint con = conDef.getConstraint(); + if (con instanceof RMListOfValuesConstraint) + { + RMListOfValuesConstraint rmCon = ((RMListOfValuesConstraint)con); + String conName = rmCon.getShortName(); + MatchLogic matchLogic = rmCon.getMatchLogicEnum(); + Map> caveatConstraintDef = caveatConfig.get(conName); + if (caveatConstraintDef == null) + { + continue; + } + else + { + Set userGroupNames = authorityService.getAuthoritiesForUser(userName); + List allowedValues = getRMAllowedValues(userName, userGroupNames, conName); + + List propValues = null; + Object val = entry.getValue(); + if (val instanceof String) + { + propValues = new ArrayList(1); + propValues.add((String)val); + } + else if (val instanceof List) + { + propValues = (List)val; + } + + if (propValues != null && !isAllowed(propValues, allowedValues, matchLogic)) + { + if (logger.isDebugEnabled()) + { + logger.debug("Veto access: caveat="+conName+", userName="+userName+", nodeRef="+nodeRef+", propName="+propName+", propValues="+propValues+", allowedValues="+allowedValues); + } + return false; + } + } + } + } + } + } + } + + return true; + } + } + + private boolean isAllowed(List propValues, List userGroupValues, MatchLogic matchLogic) + { + if (matchLogic.equals(MatchLogic.AND)) + { + // check user/group values match all values on node + for (String propValue : propValues) + { + if (! userGroupValues.contains(propValue)) + { + if (logger.isTraceEnabled()) + { + logger.trace("Not allowed: "+propValues+", "+userGroupValues+", "+matchLogic); + } + + return false; + } + } + + return true; + } + else if (matchLogic.equals(MatchLogic.OR)) + { + // check user/group values match at least one value on node + for (String propValue : propValues) + { + if (userGroupValues.contains(propValue)) + { + return true; + } + } + + if (logger.isTraceEnabled()) + { + logger.trace("Not allowed: "+propValues+", "+userGroupValues+", "+matchLogic); + } + + return false; + } + + logger.error("Unexpected match logic type: "+matchLogic); + return false; + } + + /** + * Add a single value to an authority in a list. The existing values of the list remain. + * + * @param listName the name of the RMConstraintList + * @param authorityName + * @param values + * @throws AlfrescoRuntimeException if either the list or the authority do not already exist. + */ + public void addRMConstraintListValue(String listName, String authorityName, String value) + { + Map> members = caveatConfig.get(listName); + if(members == null) + { + throw new AlfrescoRuntimeException("unable to add to list, list not defined:"+ listName); + } + List values = members.get(authorityName); + if(values == null) + { + throw new AlfrescoRuntimeException("Unable to add to authority in list. Authority not member listName: "+ listName + " authorityName:" +authorityName); + } + values.add(value); + updateOrCreateCaveatConfig(convertToJSONString(caveatConfig)); + } + + /** + * Get the member details of the specified list + * @param listName + * @return the details of the specified list + */ + public Map> getListDetails(String listName) + { + return caveatConfig.get(listName); + } + + public List getRMCaveatModels() + { + return caveatModelQNames; + } + + /** + * Replace the values for an authority in a list. + * The existing values are removed. + * + * If the authority does not already exist in the list, it will be added + * + * @param listName the name of the RMConstraintList + * @param authorityName + * @param values + */ + public void updateRMConstraintListAuthority(String listName, String authorityName, Listvalues) + { + Map> members = caveatConfig.get(listName); + if(members == null) + { + // Create the new list, with the authority name + Map> constraint = new HashMap>(0); + constraint.put(authorityName, values); + caveatConfig.put(listName, constraint); + } + else + { + members.put(authorityName, values); + } + + updateOrCreateCaveatConfig(convertToJSONString(caveatConfig)); + } + + /** + * Replace the authorities for a value in a list + * + * @param listName + * @param valueName + * @param authorities + */ + public void updateRMConstraintListValue(String listName, String valueName, Listauthorities) + { + + // members contains member, values[] + Map> members = caveatConfig.get(listName); + + if(members == null) + { + // Members List does not exist + Map> emptyConstraint = new HashMap>(0); + caveatConfig.put(listName, emptyConstraint); + members = emptyConstraint; + + } + // authorities contains authority, values[] + // pivot contains value, members[] + Map> pivot = PivotUtil.getPivot(members); + + // remove all authorities which have this value + List existingAuthorities = pivot.get(valueName); + if(existingAuthorities != null) + { + for(String authority : existingAuthorities) + { + List vals = members.get(authority); + vals.remove(valueName); + } + } + // add the new authorities for this value + for(String authority : authorities) + { + List vals = members.get(authority); + if(vals == null) + { + vals= new ArrayList(); + members.put(authority, vals); + } + vals.add(valueName); + } + + updateOrCreateCaveatConfig(convertToJSONString(caveatConfig)); + } + + public void removeRMConstraintListValue(String listName, String valueName) + { + // members contains member, values[] + Map> members = caveatConfig.get(listName); + + if(members == null) + { + // list does not exist + } + else + { + // authorities contains authority, values[] + // pivot contains value, members[] + Map> pivot = PivotUtil.getPivot(members); + + // remove all authorities which have this value + List existingAuthorities = pivot.get(valueName); + if(existingAuthorities != null) + { + for(String authority : existingAuthorities) + { + List vals = members.get(authority); + vals.remove(valueName); + } + } + + updateOrCreateCaveatConfig(convertToJSONString(caveatConfig)); + } + } + + /** + * Remove an authority from a list + * + * @param listName the name of the RMConstraintList + * @param authorityName + * @param values + */ + public void removeRMConstraintListAuthority(String listName, String authorityName) + { + Map> members = caveatConfig.get(listName); + if(members != null) + { + members.remove(listName); + } + + updateOrCreateCaveatConfig(convertToJSONString(caveatConfig)); + } + + /** + * @param config the configuration to convert + * @return a String containing the JSON representation of the configuration. + */ + private String convertToJSONString(Map>> config) + { + JSONObject obj = new JSONObject(); + + try + { + Set listNames = config.keySet(); + for(String listName : listNames) + { + Map> members = config.get(listName); + + Set authorityNames = members.keySet(); + JSONObject listMembers = new JSONObject(); + + for(String authorityName : authorityNames) + { + listMembers.put(authorityName, members.get(authorityName)); + } + + obj.put(listName, listMembers); + } + } + catch (JSONException je) + { + throw new AlfrescoRuntimeException("Invalid caveat config syntax: "+ je); + } + return obj.toString(); + } + + /** + * Get an RMConstraintInfo + * @param listQName + * @return the constraint or null if it does not exist + */ + public RMConstraintInfo getRMConstraint(QName listQName) + { + ConstraintDefinition dictionaryDef = dictionaryService.getConstraint(listQName); + if(dictionaryDef != null) + { + Constraint con = dictionaryDef.getConstraint(); + if (con instanceof RMListOfValuesConstraint) + { + final RMListOfValuesConstraint def = (RMListOfValuesConstraint)con; + + RMConstraintInfo info = new RMConstraintInfo(); + info.setName(listQName.toPrefixString()); + info.setTitle(con.getTitle()); + List allowedValues = AuthenticationUtil.runAs(new RunAsWork>() + { + public List doWork() + { + return def.getAllowedValues(); + } + }, AuthenticationUtil.getSystemUserName()); + + info.setAllowedValues(allowedValues.toArray(new String[allowedValues.size()])); + info.setCaseSensitive(def.isCaseSensitive()); + return info; + } + } + return null; + } + + /** + * Get RM Constraint detail. + * + * @return the constraintInfo or null + */ + public RMConstraintInfo getRMConstraint(String listName) + { + QName listQName = QName.createQName(listName, namespaceService); + return getRMConstraint(listQName); + } + + public void deleteRMConstraint(String listName) + { + caveatConfig.remove(listName); + updateOrCreateCaveatConfig(convertToJSONString(caveatConfig)); + } + + public void addRMConstraint(String listName) + { + Map> emptyConstraint = new HashMap>(0); + caveatConfig.put(listName, emptyConstraint); + updateOrCreateCaveatConfig(convertToJSONString(caveatConfig)); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/RMCaveatConfigService.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/RMCaveatConfigService.java new file mode 100644 index 0000000000..73e33738f5 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/RMCaveatConfigService.java @@ -0,0 +1,153 @@ +/* + * 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.module.org_alfresco_module_rm.caveat; + +import java.io.File; +import java.io.InputStream; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.service.cmr.repository.NodeRef; + +public interface RMCaveatConfigService +{ + public void init(); + + /** + * Get allowed values for given caveat list (for current user) + * @param constraintName + * @return + */ + public List getRMAllowedValues(String constraintName); + + /** + * Check whether access to 'record component' node is vetoed for current user due to caveat(s) + * + * @param nodeRef + * @return false, if caveat(s) veto access otherwise return true + */ + public boolean hasAccess(NodeRef nodeRef); + + /* + * Get a single RM constraint + */ + public RMConstraintInfo getRMConstraint(String listName); + + /* + * Get the names of all the caveat lists + */ + public Set getAllRMConstraints(); + + /** + * Get the details of a caveat list + * @param listName + * @return + */ + public Map> getListDetails(String listName); + + public NodeRef updateOrCreateCaveatConfig(File jsonFile); + + public NodeRef updateOrCreateCaveatConfig(String jsonString); + + public NodeRef updateOrCreateCaveatConfig(InputStream is); + + /** + * add RM constraint list + * @param listName the name of the RMConstraintList + * @param listTitle + */ + public RMConstraintInfo addRMConstraint(String listName, String listTitle, String[] allowedValues); + + /** + * update RM constraint list allowed values + * @param listName the name of the RMConstraintList - can not be changed + * @param allowedValues + */ + public RMConstraintInfo updateRMConstraintAllowedValues(String listName, String[] allowedValues); + + /** + * update RM constraint Title + * @param listName the name of the RMConstraintList - can not be changed + * @param allowedValues + */ + public RMConstraintInfo updateRMConstraintTitle(String listName, String newTitle); + + + /** + * delete RM Constraint + * + * @param listName the name of the RMConstraintList + */ + public void deleteRMConstraint(String listName); + + /** + * Add a single value to an authority in a list. The existing values of the list remain. + * + * @param listName the name of the RMConstraintList + * @param authorityName + * @param values + * @throws AlfrescoRuntimeException if either the list or the authority do not already exist. + */ + public void addRMConstraintListValue(String listName, String authorityName, String value); + + /** + * Replace the values for an authority in a list. + * The existing values are removed. + * + * If the authority does not already exist in the list, it will be added + * + * @param listName the name of the RMConstraintList + * @param authorityName + * @param values + */ + public void updateRMConstraintListAuthority(String listName, String authorityName, Listvalues); + + /** + * Remove an authority from a list + * + * @param listName the name of the RMConstraintList + * @param authorityName + * @param values + */ + public void removeRMConstraintListAuthority(String listName, String authorityName); + + /** + * Replace the values for an authority in a list. + * The existing values are removed. + * + * If the authority does not already exist in the list, it will be added + * + * @param listName the name of the RMConstraintList + * @param value + * @param authorities + */ + public void updateRMConstraintListValue(String listName, String value, Listauthorities); + + /** + * Remove an authority from a list + * + * @param listName the name of the RMConstraintList + * @param authorityName + * @param value + */ + public void removeRMConstraintListValue(String listName, String valueName); + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/RMCaveatConfigServiceImpl.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/RMCaveatConfigServiceImpl.java new file mode 100644 index 0000000000..33f13f4dc0 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/RMCaveatConfigServiceImpl.java @@ -0,0 +1,427 @@ +/* + * 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.module.org_alfresco_module_rm.caveat; + +import java.io.File; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminService; +import org.alfresco.module.org_alfresco_module_rm.caveat.RMListOfValuesConstraint.MatchLogic; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.service.cmr.dictionary.Constraint; +import org.alfresco.service.cmr.dictionary.ConstraintDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * RM Caveat Config Service impl + * + * @author janv + */ +public class RMCaveatConfigServiceImpl implements RMCaveatConfigService +{ + private static Log logger = LogFactory.getLog(RMCaveatConfigServiceImpl.class); + + private NamespaceService namespaceService; + private DictionaryService dictionaryService; + + private RMCaveatConfigComponent rmCaveatConfigComponent; + private RecordsManagementAdminService recordsManagementAdminService; + + + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + public void setCaveatConfigComponent(RMCaveatConfigComponent rmCaveatConfigComponent) + { + this.rmCaveatConfigComponent = rmCaveatConfigComponent; + } + + public void setRecordsManagementAdminService(RecordsManagementAdminService recordsManagementAdminService) + { + this.recordsManagementAdminService = recordsManagementAdminService; + } + + public RecordsManagementAdminService getRecordsManagementAdminService() + { + return recordsManagementAdminService; + } + + public void init() + { + rmCaveatConfigComponent.init(); + } + + public NodeRef updateOrCreateCaveatConfig(InputStream is) + { + return rmCaveatConfigComponent.updateOrCreateCaveatConfig(is); + } + + public NodeRef updateOrCreateCaveatConfig(File jsonFile) + { + return rmCaveatConfigComponent.updateOrCreateCaveatConfig(jsonFile); + } + + public NodeRef updateOrCreateCaveatConfig(String jsonString) + { + return rmCaveatConfigComponent.updateOrCreateCaveatConfig(jsonString); + } + + // Get allowed values for given caveat (for current user) + public List getRMAllowedValues(String constraintName) + { + return rmCaveatConfigComponent.getRMAllowedValues(constraintName); + } + + /** + * Check whether access to 'record component' node is vetoed for current user due to caveat(s) + * + * @param nodeRef + * @return false, if caveat(s) veto access otherwise return true + */ + public boolean hasAccess(NodeRef nodeRef) + { + return rmCaveatConfigComponent.hasAccess(nodeRef); + } + + /** + * add RM constraint list + * @param listName the name of the RMConstraintList + */ + public RMConstraintInfo addRMConstraint(String listName, String title, String[] values) + { + return addRMConstraint(listName, title, values, MatchLogic.AND); + } + + public RMConstraintInfo addRMConstraint(String listName, String title, String[] values, MatchLogic matchLogic) + { + if(listName == null) + { + // Generate a list name + // FIXME: hardcoded namespace + listName = "rmc:" + UUID.randomUUID().toString(); + } + + ListallowedValues = new ArrayList(); + for(String value : values) + { + allowedValues.add(value); + } + + QName listQName = QName.createQName(listName, namespaceService); + + // TEMP review - if it already exists then change it for now + try + { + recordsManagementAdminService.addCustomConstraintDefinition(listQName, title, true, allowedValues, matchLogic); + } + catch (AlfrescoRuntimeException e) + { + if (e.getMessage().contains("Constraint already exists")) + { + recordsManagementAdminService.changeCustomConstraintValues(listQName, allowedValues); + recordsManagementAdminService.changeCustomConstraintTitle(listQName, title); + } + } + + rmCaveatConfigComponent.addRMConstraint(listName); + + RMConstraintInfo info = new RMConstraintInfo(); + info.setName(listQName.toPrefixString()); + info.setTitle(title); + info.setAllowedValues(values); + info.setCaseSensitive(true); + return info; + } + + /** + * delete RM Constraint List + * + * @param listName the name of the RMConstraintList + */ + public void deleteRMConstraint(String listName) + { + rmCaveatConfigComponent.deleteRMConstraint(listName); + + QName listQName = QName.createQName(listName, namespaceService); + + recordsManagementAdminService.removeCustomConstraintDefinition(listQName); + } + + /** + * Add a single value to an authority in a list. The existing values of the list remain. + * + * @param listName the name of the RMConstraintList + * @param authorityName + * @param values + * @throws AlfrescoRuntimeException if either the list or the authority do not already exist. + */ + public void addRMConstraintListValue(String listName, String authorityName, String value) + { + rmCaveatConfigComponent.addRMConstraintListValue(listName, authorityName, value); + } + + /** + * Get the details of the specified list + * @param listName + * @return the details of the specified list + */ + public Map> getListDetails(String listName) + { + return rmCaveatConfigComponent.getListDetails(listName); + } + + /** + * Replace the values for an authority in a list. + * The existing values are removed. + * + * If the authority does not already exist in the list, it will be added + * + * @param listName the name of the RMConstraintList + * @param authorityName + * @param values + */ + public void updateRMConstraintListAuthority(String listName, String authorityName, Listvalues) + { + rmCaveatConfigComponent.updateRMConstraintListAuthority(listName, authorityName, values); + } + + /** + * Replace the authorities for a value in a list + * + * @param listName + * @param valueName + * @param authorities + */ + public void updateRMConstraintListValue(String listName, String valueName, Listauthorities) + { + rmCaveatConfigComponent.updateRMConstraintListValue(listName, valueName, authorities); + } + + /** + * Remove an authority from a list + * + * @param listName the name of the RMConstraintList + * @param authorityName + * @param values + */ + public void removeRMConstraintListAuthority(String listName, String authorityName) + { + rmCaveatConfigComponent.removeRMConstraintListAuthority(listName, authorityName); + } + + /** + * Get all Constraint Lists + */ + public Set getAllRMConstraints() + { + Set info = new HashSet(); + + List defs = new ArrayList(10); + for (QName caveatModelQName : rmCaveatConfigComponent.getRMCaveatModels()) + { + defs.addAll(recordsManagementAdminService.getCustomConstraintDefinitions(caveatModelQName)); + } + + for(ConstraintDefinition dictionaryDef : defs) + { + Constraint con = dictionaryDef.getConstraint(); + if (con instanceof RMListOfValuesConstraint) + { + final RMListOfValuesConstraint def = (RMListOfValuesConstraint)con; + RMConstraintInfo i = new RMConstraintInfo(); + i.setName(def.getShortName()); + i.setTitle(def.getTitle()); + + // note: assumes only one caveat/LOV against a given property + List allowedValues = AuthenticationUtil.runAs(new RunAsWork>() + { + public List doWork() + { + return def.getAllowedValues(); + } + }, AuthenticationUtil.getSystemUserName()); + + i.setAllowedValues(allowedValues.toArray(new String[allowedValues.size()])); + i.setCaseSensitive(def.isCaseSensitive()); + info.add(i); + } + + } + + return info; + } + + /** + * Get an RMConstraintInfo + * @param listQName + * @return the constraint or null if it does not exist + */ + public RMConstraintInfo getRMConstraint(QName listQName) + { + ConstraintDefinition dictionaryDef = dictionaryService.getConstraint(listQName); + if(dictionaryDef != null) + { + Constraint con = dictionaryDef.getConstraint(); + if (con instanceof RMListOfValuesConstraint) + { + final RMListOfValuesConstraint def = (RMListOfValuesConstraint)con; + + RMConstraintInfo info = new RMConstraintInfo(); + info.setName(listQName.toPrefixString()); + info.setTitle(con.getTitle()); + List allowedValues = AuthenticationUtil.runAs(new RunAsWork>() + { + public List doWork() + { + return def.getAllowedValues(); + } + }, AuthenticationUtil.getSystemUserName()); + + info.setAllowedValues(allowedValues.toArray(new String[allowedValues.size()])); + info.setCaseSensitive(def.isCaseSensitive()); + return info; + } + } + return null; + } + + /** + * Get RM Constraint detail. + * + * @return the constraintInfo or null + */ + public RMConstraintInfo getRMConstraint(String listName) + { + QName listQName = QName.createQName(listName, namespaceService); + return getRMConstraint(listQName); + + } + + /** + * Update The allowed values for an RM Constraint. + * + * @param listName The name of the list. + * @param allowedValues the new alowed values + * + */ + public RMConstraintInfo updateRMConstraintAllowedValues(String listName, String[] allowedValues) + { + QName listQName = QName.createQName(listName, namespaceService); + + if(allowedValues != null) + { + ListallowedValueList = new ArrayList(); + for(String value : allowedValues) + { + allowedValueList.add(value); + } + + ConstraintDefinition dictionaryDef = dictionaryService.getConstraint(listQName); + Constraint con = dictionaryDef.getConstraint(); + if (con instanceof RMListOfValuesConstraint) + { + final RMListOfValuesConstraint def = (RMListOfValuesConstraint)con; + List oldAllowedValues = AuthenticationUtil.runAs(new RunAsWork>() + { + public List doWork() + { + return def.getAllowedValues(); + } + }, AuthenticationUtil.getSystemUserName()); + + /** + * Deal with any additions + */ + for(String newValue : allowedValueList) + { + if(!oldAllowedValues.contains(newValue)) + { + // This is an addition + if(logger.isDebugEnabled()) + { + logger.debug("value added to list:" + listQName + ":" + newValue); + } + } + } + + /** + * Deal with any deletions + */ + for(String oldValue : oldAllowedValues) + { + if(!allowedValueList.contains(oldValue)) + { + // This is a deletion + if(logger.isDebugEnabled()) + { + logger.debug("value removed from list:" + listQName + ":" + oldValue); + } + removeRMConstraintListValue(listName, oldValue); + } + } + } + + recordsManagementAdminService.changeCustomConstraintValues(listQName, allowedValueList); + } + + return getRMConstraint(listName); + } + + /** + * Remove a value from a list and cascade delete. + */ + public void removeRMConstraintListValue(String listName, String valueName) + { + //TODO need to update the rm constraint definition + // recordsManagementAdminService. + + rmCaveatConfigComponent.removeRMConstraintListValue(listName, valueName); + } + + /** + * Update the title of this RM Constraint. + */ + public RMConstraintInfo updateRMConstraintTitle(String listName, String newTitle) + { + QName listQName = QName.createQName(listName, namespaceService); + + recordsManagementAdminService.changeCustomConstraintTitle(listQName, newTitle); + return getRMConstraint(listName); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/RMConstraintInfo.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/RMConstraintInfo.java new file mode 100644 index 0000000000..76d609df2a --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/RMConstraintInfo.java @@ -0,0 +1,63 @@ +/* + * 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.module.org_alfresco_module_rm.caveat; + +public class RMConstraintInfo +{ + private String name; + private String title; + private boolean caseSensitive; + private String[] allowedValues; + + public void setName(String name) + { + this.name = name; + } + public String getName() + { + return name; + } + public void setTitle(String title) + { + this.title = title; + } + public String getTitle() + { + return title; + } + public void setCaseSensitive(boolean caseSensitive) + { + this.caseSensitive = caseSensitive; + } + public boolean isCaseSensitive() + { + return caseSensitive; + } + public void setAllowedValues(String[] values) + { + this.allowedValues = values; + } + public String[] getAllowedValues() + { + return allowedValues; + } + + + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/RMListOfValuesConstraint.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/RMListOfValuesConstraint.java new file mode 100644 index 0000000000..47ffa3fcbf --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/RMListOfValuesConstraint.java @@ -0,0 +1,229 @@ +/* + * 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.module.org_alfresco_module_rm.caveat; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.repo.dictionary.constraint.ListOfValuesConstraint; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.service.cmr.dictionary.ConstraintException; +import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; +import org.alfresco.service.cmr.repository.datatype.TypeConversionException; + +/** + * RM Constraint implementation that ensures the value is one of a constrained + * list of values. By default, this constraint is case-sensitive. + * + * @see #setAllowedValues(List) + * @see #setCaseSensitive(boolean) + * + * @author janv + */ +public class RMListOfValuesConstraint extends ListOfValuesConstraint +{ + //private static final String ERR_NO_VALUES = "d_dictionary.constraint.list_of_values.no_values"; + private static final String ERR_NON_STRING = "d_dictionary.constraint.string_length.non_string"; + private static final String ERR_INVALID_VALUE = "d_dictionary.constraint.list_of_values.invalid_value"; + + private List allowedValues; + private List allowedValuesUpper; + private MatchLogic matchLogic = MatchLogic.AND; // defined match logic used by caveat matching (default = "AND") + + public enum MatchLogic + { + AND, // closed marking - all values must match + OR; // open marking - at least one value must match + } + + // note: alternative to static init could be to use 'registered' constraint + private static RMCaveatConfigService caveatConfigService; + + public void setCaveatConfigService(RMCaveatConfigService caveatConfigService) + { + RMListOfValuesConstraint.caveatConfigService = caveatConfigService; + } + + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(80); + sb.append("RMListOfValuesConstraint") + .append("[allowedValues=").append(getAllowedValues()) + .append(", caseSensitive=").append(isCaseSensitive()) + .append(", sorted=").append(isCaseSensitive()) + .append(", matchLogic=").append(getMatchLogic()) + .append("]"); + return sb.toString(); + } + + public RMListOfValuesConstraint() + { + super(); + + // Set RM list of value constraints to be sorted by default + sorted = true; + } + + /** + * Get the allowed values. Note that these are String instances, but may + * represent non-String values. It is up to the caller to distinguish. + * + * @return Returns the values allowed + */ + @Override + public List getRawAllowedValues() + { + String runAsUser = AuthenticationUtil.getRunAsUser(); + if ((runAsUser != null) && (! runAsUser.equals(AuthenticationUtil.getSystemUserName())) && (caveatConfigService != null)) + { + List allowedForUser = caveatConfigService.getRMAllowedValues(getShortName()); // get allowed values for current user + + List filteredList = new ArrayList(allowedForUser.size()); + for (String allowed : allowedForUser) + { + if (this.allowedValues.contains(allowed)) + { + filteredList.add(allowed); + } + } + + return filteredList; + } + else + { + return this.allowedValues; + } + } + + private List getAllowedValuesUpper() + { + String runAsUser = AuthenticationUtil.getRunAsUser(); + if ((runAsUser != null) && (! runAsUser.equals(AuthenticationUtil.getSystemUserName())) && (caveatConfigService != null)) + { + List allowedForUser = caveatConfigService.getRMAllowedValues(getType()); // get allowed values for current user + + List filteredList = new ArrayList(allowedForUser.size()); + for (String allowed : allowedForUser) + { + if (this.allowedValuesUpper.contains(allowed.toUpperCase())) + { + filteredList.add(allowed); + } + } + + return filteredList; + } + else + { + return this.allowedValuesUpper; + } + } + /** + * Set the values that are allowed by the constraint. + * + * @param values a list of allowed values + */ + @SuppressWarnings("unchecked") + @Override + public void setAllowedValues(List allowedValues) + { + if (allowedValues == null) + { + allowedValues = new ArrayList(0); + } + int valueCount = allowedValues.size(); + this.allowedValues = Collections.unmodifiableList(allowedValues); + + // make the upper case versions + this.allowedValuesUpper = new ArrayList(valueCount); + for (String allowedValue : this.allowedValues) + { + allowedValuesUpper.add(allowedValue.toUpperCase()); + } + } + + @Override + public void initialize() + { + checkPropertyNotNull("allowedValues", allowedValues); + } + + @Override + public Map getParameters() + { + Map params = new HashMap(2); + + params.put("caseSensitive", isCaseSensitive()); + params.put("allowedValues", getAllowedValues()); + params.put("sorted", isSorted()); + params.put("matchLogic", getMatchLogic()); + + return params; + } + + public MatchLogic getMatchLogicEnum() + { + return matchLogic; + } + + public String getMatchLogic() + { + return matchLogic.toString(); + } + + public void setMatchLogic(String matchLogicStr) + { + this.matchLogic = MatchLogic.valueOf(matchLogicStr); + } + + @Override + protected void evaluateSingleValue(Object value) + { + // convert the value to a String + String valueStr = null; + try + { + valueStr = DefaultTypeConverter.INSTANCE.convert(String.class, value); + } + catch (TypeConversionException e) + { + throw new ConstraintException(ERR_NON_STRING, value); + } + // check that the value is in the set of allowed values + if (isCaseSensitive()) + { + if (!getAllowedValues().contains(valueStr)) + { + throw new ConstraintException(ERR_INVALID_VALUE, value); + } + } + else + { + if (!getAllowedValuesUpper().contains(valueStr.toUpperCase())) + { + throw new ConstraintException(ERR_INVALID_VALUE, value); + } + } + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/ScriptAuthority.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/ScriptAuthority.java new file mode 100644 index 0000000000..b615d1434a --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/ScriptAuthority.java @@ -0,0 +1,51 @@ +/* + * 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.module.org_alfresco_module_rm.caveat; + +import java.io.Serializable; + + +public class ScriptAuthority implements Serializable +{ + /** + * + */ + private static final long serialVersionUID = 1L; + private String authorityTitle; + private String authorityName; + + public void setAuthorityName(String authorityName) + { + this.authorityName = authorityName; + } + public String getAuthorityName() + { + return authorityName; + } + public void setAuthorityTitle(String authorityName) + { + this.authorityTitle = authorityName; + } + public String getAuthorityTitle() + { + return authorityTitle; + } + + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/ScriptConstraint.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/ScriptConstraint.java new file mode 100644 index 0000000000..2c3598d20d --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/ScriptConstraint.java @@ -0,0 +1,264 @@ +/* + * 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.module.org_alfresco_module_rm.caveat; + +import java.io.Serializable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.List; +import java.util.Set; + +import org.alfresco.service.cmr.security.AuthorityService; +import org.json.JSONArray; +import org.json.JSONObject; + +public class ScriptConstraint implements Serializable +{ + /** + * + */ + private static final long serialVersionUID = 1L; + + private RMConstraintInfo info; + + private RMCaveatConfigService rmCaveatconfigService; + + private AuthorityService authorityService; + + ScriptConstraint(RMConstraintInfo info, RMCaveatConfigService rmCaveatconfigService, AuthorityService authorityService) + { + this.info = info; + this.rmCaveatconfigService = rmCaveatconfigService; + this.authorityService = authorityService; + } + + public void setTitle(String title) + { + info.setTitle(title); + } + public String getTitle() + { + return info.getTitle(); + } + public void setName(String name) + { + info.setName(name); + } + + public String getName() + { + String xxx = info.getName().replace(":", "_"); + return xxx; + } + + public boolean isCaseSensitive() + { + return info.isCaseSensitive(); + } + + public String[] getAllowedValues() + { + return info.getAllowedValues(); + } + + public ScriptConstraintAuthority[] getAuthorities() + { + Map> values = rmCaveatconfigService.getListDetails(info.getName()); + + if (values == null) + { + return new ScriptConstraintAuthority[0]; + } + + // Here with some data to return + Set authorities = values.keySet(); + + ArrayList constraints = new ArrayList(values.size()); + for(String authority : authorities) + { + ScriptConstraintAuthority constraint = new ScriptConstraintAuthority(); + constraint.setAuthorityName(authority); + constraint.setValues(values.get(authority)); + constraints.add(constraint); + } + + ScriptConstraintAuthority[] retVal = constraints.toArray(new ScriptConstraintAuthority[constraints.size()]); + + return retVal; + } + + /** + * updateTitle + */ + public void updateTitle(String newTitle) + { + info.setTitle(newTitle); + rmCaveatconfigService.updateRMConstraintTitle(info.getName(), newTitle) ; + } + + /** + * updateAllowedValues + */ + public void updateAllowedValues(String[] allowedValues) + { + info.setAllowedValues(allowedValues); + rmCaveatconfigService.updateRMConstraintAllowedValues(info.getName(), allowedValues); + } + + /** + * Update a value + * @param values + * @param authorities + */ + public void updateValues(JSONArray bodge) throws Exception + { + for(int i = 0; i < bodge.length(); i++) + { + + JSONObject obj = bodge.getJSONObject(i); + String value = obj.getString("value"); + JSONArray authorities = obj.getJSONArray("authorities"); + List aList = new ArrayList(); + for(int j = 0; j < authorities.length();j++) + { + aList.add(authorities.getString(j)); + } + rmCaveatconfigService.updateRMConstraintListValue(info.getName(), value, aList); + } + } + + /** + * Update a value + * @param values + * @param authorities + */ + public void updateValues(String value, String[] authorities) + { + List list = Arrays.asList(authorities); + rmCaveatconfigService.updateRMConstraintListValue(info.getName(), value, list); + } + + /** + * Cascade delete an authority + * @param authority + */ + public void deleteAuthority(String authority) + { + + } + + /** + * Cascade delete a value + * @param value + */ + public void deleteValue(String value) + { + + } + + + /** + * Get a single value + * @param value + * @return + */ + public ScriptConstraintValue getValue(String value) + { + ScriptConstraintValue[] values = getValues(); + + for(ScriptConstraintValue val : values) + { + if(val.getValueName().equalsIgnoreCase(value)) + { + return val; + } + } + return null; + } + + public ScriptConstraintValue[] getValues() + { + // authority, values + Map> details = rmCaveatconfigService.getListDetails(info.getName()); + + if (details == null) + { + details = new HashMap>(); + } + + // values, authorities + Map> pivot = PivotUtil.getPivot(details); + + // Here with some data to return + Set values = pivot.keySet(); + + ArrayList constraints = new ArrayList(pivot.size()); + for(String value : values) + { + ScriptConstraintValue constraint = new ScriptConstraintValue(); + constraint.setValueName(value); + constraint.setValueTitle(value); + + Listauthorities = pivot.get(value); + List sauth = new ArrayList(); + for(String authority : authorities) + { + ScriptAuthority a = new ScriptAuthority(); + a.setAuthorityName(authority); + + String displayName = authorityService.getAuthorityDisplayName(authority); + if(displayName != null) + { + a.setAuthorityTitle(displayName); + } + else + { + a.setAuthorityTitle(authority); + } + sauth.add(a); + } + constraint.setAuthorities(sauth); + constraints.add(constraint); + } + + /** + * Now go through and add any "empty" values + */ + for(String value : info.getAllowedValues()) + { + if(!values.contains(value)) + { + ScriptConstraintValue constraint = new ScriptConstraintValue(); + constraint.setValueName(value); + constraint.setValueTitle(value); + List sauth = new ArrayList(); + constraint.setAuthorities(sauth); + constraints.add(constraint); + } + } + + + ScriptConstraintValue[] retVal = constraints.toArray(new ScriptConstraintValue[constraints.size()]); + return retVal; + } + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/ScriptConstraintAuthority.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/ScriptConstraintAuthority.java new file mode 100644 index 0000000000..47d4c2a139 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/ScriptConstraintAuthority.java @@ -0,0 +1,49 @@ +/* + * 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.module.org_alfresco_module_rm.caveat; + +import java.util.List; +import java.io.Serializable; + +public class ScriptConstraintAuthority implements Serializable +{ + /** + * + */ + private static final long serialVersionUID = -4659454215122271811L; + private String authorityName; + private Listvalues; + + public void setValues(List values) + { + this.values = values; + } + public List getValues() + { + return values; + } + public void setAuthorityName(String authorityName) + { + this.authorityName = authorityName; + } + public String getAuthorityName() + { + return authorityName; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/ScriptConstraintValue.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/ScriptConstraintValue.java new file mode 100644 index 0000000000..7c7db55a7f --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/ScriptConstraintValue.java @@ -0,0 +1,57 @@ +/* + * 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.module.org_alfresco_module_rm.caveat; + +import java.util.List; +import java.io.Serializable; + +public class ScriptConstraintValue implements Serializable +{ + /** + * + */ + private static final long serialVersionUID = -4659454215122271811L; + private String value; + private Listauthorities; + + public void setAuthorities(List values) + { + this.authorities = values; + } + public List getAuthorities() + { + return authorities; + } + public void setValueName(String authorityName) + { + this.value = authorityName; + } + public String getValueName() + { + return value; + } + public void setValueTitle(String authorityName) + { + this.value = authorityName; + } + public String getValueTitle() + { + return value; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/ScriptRMCaveatConfigService.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/ScriptRMCaveatConfigService.java new file mode 100644 index 0000000000..740b72a546 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/caveat/ScriptRMCaveatConfigService.java @@ -0,0 +1,166 @@ +/* + * 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.module.org_alfresco_module_rm.caveat; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import org.alfresco.repo.jscript.BaseScopableProcessorExtension; +import org.alfresco.service.cmr.security.AuthorityService; + +/** + * Script projection of RM Caveat Config Service + * + * @author Mark Rogers + */ +public class ScriptRMCaveatConfigService extends BaseScopableProcessorExtension +{ + private RMCaveatConfigService caveatConfigService; + private AuthorityService authorityService; + + public void setCaveatConfigService(RMCaveatConfigService rmCaveatConfigService) + { + this.caveatConfigService = rmCaveatConfigService; + } + + public RMCaveatConfigService getRmCaveatConfigService() + { + return caveatConfigService; + } + + public void setAuthorityService(AuthorityService authorityService) + { + this.authorityService = authorityService; + } + + public AuthorityService getAuthorityService() + { + return authorityService; + } + + public ScriptConstraint getConstraint(String listName) + { + //TODO Temporary conversion + String xxx = listName.replace("_", ":"); + + RMConstraintInfo info = caveatConfigService.getRMConstraint(xxx); + + if(info != null) + { + return new ScriptConstraint(info, caveatConfigService, getAuthorityService()); + } + + return null; + } + + public ScriptConstraint[] getAllConstraints() + { + return getConstraints(true); + } + + public ScriptConstraint[] getConstraintsWithoutEmptyList() + { + return getConstraints(false); + } + + private ScriptConstraint[] getConstraints(boolean includeEmptyList) + { + Set values = caveatConfigService.getAllRMConstraints(); + + List vals = new ArrayList(values.size()); + for(RMConstraintInfo value : values) + { + ScriptConstraint c = new ScriptConstraint(value, caveatConfigService, getAuthorityService()); + if (includeEmptyList) + { + vals.add(c); + } + else + { + if (c.getValues().length > 0) + { + vals.add(c); + } + } + } + + return vals.toArray(new ScriptConstraint[vals.size()]); + } + + /** + * Delete list + * @param listName + + */ + public void deleteConstraintList(String listName) + { + //TODO Temporary conversion + String xxx = listName.replace("_", ":"); + caveatConfigService.deleteRMConstraint(xxx); + } + + + + /** + * Update value + */ + public void updateConstraintValues(String listName, String authorityName, String[]values) + { + List vals = new ArrayList(); + caveatConfigService.updateRMConstraintListAuthority(listName, authorityName, vals); + } + + /** + * Delete the constraint values. i.e remove an authority from a constraint list + */ + public void deleteRMConstraintListAuthority(String listName, String authorityName) + { + //TODO Temporary conversion + String xxx = listName.replace("_", ":"); + + caveatConfigService.removeRMConstraintListAuthority(xxx, authorityName); + } + + /** + * Delete the constraint values. i.e remove a value from a constraint list + */ + public void deleteRMConstraintListValue(String listName, String valueName) + { + //TODO Temporary conversion + String xxx = listName.replace("_", ":"); + + caveatConfigService.removeRMConstraintListValue(xxx, valueName); + + } + + public ScriptConstraint createConstraint(String listName, String title, String[] allowedValues) + { + //TODO Temporary conversion + if(listName != null) + { + listName = listName.replace("_", ":"); + } + + RMConstraintInfo info = caveatConfigService.addRMConstraint(listName, title, allowedValues); + ScriptConstraint c = new ScriptConstraint(info, caveatConfigService, getAuthorityService()); + return c; + } + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionAction.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionAction.java new file mode 100644 index 0000000000..212c8d199b --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionAction.java @@ -0,0 +1,94 @@ +/* + * 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.module.org_alfresco_module_rm.disposition; + +import java.util.Date; +import java.util.List; + +import org.alfresco.module.org_alfresco_module_rm.event.EventCompletionDetails; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Disposition action interface. + * + * @author Roy Wetherall + */ +public interface DispositionAction +{ + /** + * @return the node reference + */ + NodeRef getNodeRef(); + + /** + * @return the disposition action definition + */ + DispositionActionDefinition getDispositionActionDefinition(); + + /** + * @return the id of the action + */ + String getId(); + + /** + * @return the name of the action + */ + String getName(); + + /** + * @return the display label for the action + */ + String getLabel(); + + /** + * @return the dispostion action as of eligibility date + */ + Date getAsOfDate(); + + /** + * @return true if the events are complete (ie: enough events have been completed to make the disposition + * action + */ + boolean isEventsEligible(); + + /** + * @return the user that started the action + */ + String getStartedBy(); + + /** + * @return when the action was started + */ + Date getStartedAt(); + + /** + * @return the user that completed the action + */ + String getCompletedBy(); + + /** + * @return when the action was completed + */ + Date getCompletedAt(); + + /** + * @return List of events that need to be completed for the action + */ + List getEventCompletionDetails(); +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionActionDefinition.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionActionDefinition.java new file mode 100644 index 0000000000..bdac5e0218 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionActionDefinition.java @@ -0,0 +1,112 @@ +/* + * 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.module.org_alfresco_module_rm.disposition; + +import java.util.List; + +import org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEvent; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.Period; +import org.alfresco.service.namespace.QName; + +/** + * Disposition action interface + * + * @author Roy Wetherall + */ +public interface DispositionActionDefinition +{ + /** + * Get the NodeRef that represents the disposition action definition + * + * @return NodeRef of disposition action definition + */ + NodeRef getNodeRef(); + + /** + * Get disposition action id + * + * @return String id + */ + String getId(); + + /** + * Get the index of the action within the disposition instructions + * + * @return int disposition action index + */ + int getIndex(); + + /** + * Get the name of disposition action + * + * @return String name + */ + String getName(); + + /** + * Get the display label of the disposition action + * + * @return String name's display label + */ + String getLabel(); + + /** + * Get the description of the disposition action + * + * @return String description + */ + String getDescription(); + + /** + * Get the period for the disposition action + * + * @return Period disposition period + */ + Period getPeriod(); + + /** + * Property to which the period is relative to + * + * @return QName property name + */ + QName getPeriodProperty(); + + /** + * List of events for the disposition + * + * @return List list of events + */ + List getEvents(); + + /** + * Indicates whether the disposition action is eligible when the earliest event is complete, otherwise + * all events must be complete before eligibility. + * + * @return boolean true if eligible on first action complete, false otherwise + */ + boolean eligibleOnFirstCompleteEvent(); + + /** + * Get the location of the disposition (can be null) + * + * @return String disposition location + */ + String getLocation(); +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionActionDefinitionImpl.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionActionDefinitionImpl.java new file mode 100644 index 0000000000..e6d4b4061b --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionActionDefinitionImpl.java @@ -0,0 +1,219 @@ +/* + * 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.module.org_alfresco_module_rm.disposition; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction; +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService; +import org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEvent; +import org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEventService; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.Period; +import org.alfresco.service.namespace.QName; + +/** + * Disposition action implementation + * + * @author Roy Wetherall + */ +public class DispositionActionDefinitionImpl implements DispositionActionDefinition, RecordsManagementModel +{ + /** Name */ + private String name; + + /** Description */ + private String description; + + /** Label */ + private String label; + + /** Node service */ + private NodeService nodeService; + + /** Records management action service */ + private RecordsManagementActionService recordsManagementActionService; + + /** Records management event service */ + private RecordsManagementEventService recordsManagementEventService; + + /** Disposition action node reference */ + private NodeRef dispositionActionNodeRef; + + /** Action index */ + private int index; + + /** + * Constructor + * + * @param services service registry + * @param nodeRef disposition action node reference + * @param index index of disposition action + */ + public DispositionActionDefinitionImpl(RecordsManagementEventService recordsManagementEventService, RecordsManagementActionService recordsManagementActionService, NodeService nodeService, NodeRef nodeRef, int index) + { + //this.services = services; + this.recordsManagementEventService = recordsManagementEventService; + this.recordsManagementActionService = recordsManagementActionService; + this.nodeService = nodeService; + this.dispositionActionNodeRef = nodeRef; + this.index = index; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionActionDefinition#getNodeRef() + */ + public NodeRef getNodeRef() + { + return this.dispositionActionNodeRef; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionActionDefinition#getIndex() + */ + public int getIndex() + { + return this.index; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionActionDefinition#getId() + */ + public String getId() + { + return this.dispositionActionNodeRef.getId(); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionActionDefinition#getDescription() + */ + public String getDescription() + { + if (description == null) + { + description = (String)nodeService.getProperty(this.dispositionActionNodeRef, PROP_DISPOSITION_DESCRIPTION); + } + return description; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionActionDefinition#getName() + */ + public String getName() + { + if (name == null) + { + name = (String)nodeService.getProperty(this.dispositionActionNodeRef, PROP_DISPOSITION_ACTION_NAME); + } + return name; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionActionDefinition#getLabel() + */ + public String getLabel() + { + if (label == null) + { + String name = getName(); + label = name; + + // get the disposition action from the RM action service + RecordsManagementAction action = recordsManagementActionService.getDispositionAction(name); + if (action != null) + { + label = action.getLabel(); + } + } + + return label; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionActionDefinition#getPeriod() + */ + public Period getPeriod() + { + return (Period)nodeService.getProperty(this.dispositionActionNodeRef, PROP_DISPOSITION_PERIOD); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionActionDefinition#getPeriodProperty() + */ + public QName getPeriodProperty() + { + QName result = null; + String value = (String)nodeService.getProperty(this.dispositionActionNodeRef, PROP_DISPOSITION_PERIOD_PROPERTY); + if (value != null) + { + result = QName.createQName(value); + } + return result; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionActionDefinition#getEvents() + */ + @SuppressWarnings("unchecked") + public List getEvents() + { + List events = null; + Collection eventNames = (Collection)nodeService.getProperty(this.dispositionActionNodeRef, PROP_DISPOSITION_EVENT); + if (eventNames != null) + { + events = new ArrayList(eventNames.size()); + for (String eventName : eventNames) + { + RecordsManagementEvent event = recordsManagementEventService.getEvent(eventName); + events.add(event); + } + } + else + { + events = java.util.Collections.EMPTY_LIST; + } + return events; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionActionDefinition#eligibleOnFirstCompleteEvent() + */ + public boolean eligibleOnFirstCompleteEvent() + { + boolean result = true; + String value = (String)nodeService.getProperty(this.dispositionActionNodeRef, PROP_DISPOSITION_EVENT_COMBINATION); + if (value != null && value.equals("and") == true) + { + result = false; + } + return result; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionActionDefinition#getLocation() + */ + public String getLocation() + { + return (String)nodeService.getProperty(this.dispositionActionNodeRef, PROP_DISPOSITION_LOCATION); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionActionImpl.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionActionImpl.java new file mode 100644 index 0000000000..bff4a0cc68 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionActionImpl.java @@ -0,0 +1,220 @@ +/* + * 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.module.org_alfresco_module_rm.disposition; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementServiceRegistry; +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction; +import org.alfresco.module.org_alfresco_module_rm.event.EventCompletionDetails; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; + +/** + * @author Roy Wetherall + */ +public class DispositionActionImpl implements DispositionAction, + RecordsManagementModel +{ + private RecordsManagementServiceRegistry services; + private NodeRef dispositionNodeRef; + private DispositionActionDefinition dispositionActionDefinition; + + /** + * Constructor + * + * @param services + * @param dispositionActionNodeRef + */ + public DispositionActionImpl(RecordsManagementServiceRegistry services, NodeRef dispositionActionNodeRef) + { + this.services = services; + this.dispositionNodeRef = dispositionActionNodeRef; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionAction#getDispositionActionDefinition() + */ + public DispositionActionDefinition getDispositionActionDefinition() + { + if (this.dispositionActionDefinition == null) + { + // Get the current action + String id = (String)services.getNodeService().getProperty(this.dispositionNodeRef, PROP_DISPOSITION_ACTION_ID); + + // Get the disposition instructions for the owning node + NodeRef recordNodeRef = this.services.getNodeService().getPrimaryParent(this.dispositionNodeRef).getParentRef(); + if (recordNodeRef != null) + { + DispositionSchedule ds = this.services.getDispositionService().getDispositionSchedule(recordNodeRef); + + if (ds != null) + { + // Get the disposition action definition + this.dispositionActionDefinition = ds.getDispositionActionDefinition(id); + } + } + } + + return this.dispositionActionDefinition; + + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionAction#getNodeRef() + */ + public NodeRef getNodeRef() + { + return this.dispositionNodeRef; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionAction#getLabel() + */ + public String getLabel() + { + String name = getName(); + String label = name; + + // get the disposition action from the RM action service + RecordsManagementAction action = this.services.getRecordsManagementActionService().getDispositionAction(name); + if (action != null) + { + label = action.getLabel(); + } + + return label; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionAction#getId() + */ + public String getId() + { + return (String)this.services.getNodeService().getProperty(this.dispositionNodeRef, PROP_DISPOSITION_ACTION_ID); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionAction#getName() + */ + public String getName() + { + return (String)this.services.getNodeService().getProperty(this.dispositionNodeRef, PROP_DISPOSITION_ACTION); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionAction#getAsOfDate() + */ + public Date getAsOfDate() + { + return (Date)this.services.getNodeService().getProperty(this.dispositionNodeRef, PROP_DISPOSITION_AS_OF); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionAction#isEventsEligible() + */ + public boolean isEventsEligible() + { + return ((Boolean)this.services.getNodeService().getProperty(this.dispositionNodeRef, PROP_DISPOSITION_EVENTS_ELIGIBLE)).booleanValue(); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionAction#getCompletedAt() + */ + public Date getCompletedAt() + { + return (Date)this.services.getNodeService().getProperty(this.dispositionNodeRef, PROP_DISPOSITION_ACTION_COMPLETED_AT); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionAction#getCompletedBy() + */ + public String getCompletedBy() + { + return (String)this.services.getNodeService().getProperty(this.dispositionNodeRef, PROP_DISPOSITION_ACTION_COMPLETED_BY); + } + + /* + * @see org.alfresco.module.org_alfresco_module_rm.DispositionAction#getStartedAt() + */ + public Date getStartedAt() + { + return (Date)this.services.getNodeService().getProperty(this.dispositionNodeRef, PROP_DISPOSITION_ACTION_STARTED_AT); + } + + /* + * @see org.alfresco.module.org_alfresco_module_rm.DispositionAction#getStartedBy() + */ + public String getStartedBy() + { + return (String)this.services.getNodeService().getProperty(this.dispositionNodeRef, PROP_DISPOSITION_ACTION_STARTED_BY); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionAction#getEventCompletionDetails() + */ + public List getEventCompletionDetails() + { + List assocs = this.services.getNodeService().getChildAssocs( + this.dispositionNodeRef, + ASSOC_EVENT_EXECUTIONS, + RegexQNamePattern.MATCH_ALL); + List result = new ArrayList(assocs.size()); + for (ChildAssociationRef assoc : assocs) + { + Map props = this.services.getNodeService().getProperties(assoc.getChildRef()); + String eventName = (String)props.get(PROP_EVENT_EXECUTION_NAME); + EventCompletionDetails ecd = new EventCompletionDetails( + assoc.getChildRef(), eventName, + this.services.getRecordsManagementEventService().getEvent(eventName).getDisplayLabel(), + getBooleanValue(props.get(PROP_EVENT_EXECUTION_AUTOMATIC), false), + getBooleanValue(props.get(PROP_EVENT_EXECUTION_COMPLETE), false), + (Date)props.get(PROP_EVENT_EXECUTION_COMPLETED_AT), + (String)props.get(PROP_EVENT_EXECUTION_COMPLETED_BY)); + result.add(ecd); + } + + return result; + } + + /** + * Helper method to deal with boolean values + * + * @param value + * @param defaultValue + * @return + */ + private boolean getBooleanValue(Object value, boolean defaultValue) + { + boolean result = defaultValue; + if (value != null && value instanceof Boolean) + { + result = ((Boolean)value).booleanValue(); + } + return result; + } + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionPeriodProperties.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionPeriodProperties.java new file mode 100644 index 0000000000..2479e6f06f --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionPeriodProperties.java @@ -0,0 +1,51 @@ +/* + * 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.module.org_alfresco_module_rm.disposition; + +import java.util.ArrayList; +import java.util.List; + +import org.alfresco.service.namespace.QName; + +/** + * Spring bean to allow configuration of properties used for calculating + * dates in disposition schedules. + * + * @author Gavin Cornwell + */ +public class DispositionPeriodProperties +{ + public static final String BEAN_NAME = "DispositionPeriodProperties"; + + private List periodProperties; + + public void setPropertyList(List propertyList) + { + periodProperties = new ArrayList(propertyList.size()); + for (String property : propertyList) + { + periodProperties.add(QName.createQName(property)); + } + } + + public List getPeriodProperties() + { + return periodProperties; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionSchedule.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionSchedule.java new file mode 100644 index 0000000000..c46177deb3 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionSchedule.java @@ -0,0 +1,82 @@ +/* + * 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.module.org_alfresco_module_rm.disposition; + +import java.util.List; + +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Disposition schedule interface + * + * @author Roy Wetherall + */ +public interface DispositionSchedule +{ + /** + * Get the NodeRef that represents the disposition schedule + * + * @return {@link NodeRef} of disposition schedule + */ + NodeRef getNodeRef(); + + /** + * Get the disposition authority + * + * @return {@link String} disposition authority + */ + String getDispositionAuthority(); + + /** + * Get the disposition instructions + * + * @return {@link String} disposition instructions + */ + String getDispositionInstructions(); + + /** + * Indicates whether the disposal occurs at record level or not + * + * @return boolean true if at record level, false otherwise + */ + boolean isRecordLevelDisposition(); + + /** + * Gets all the disposition action definitions for the schedule + * + * @return List<{@link DispositionActionDefinition}> disposition action definitions + */ + List getDispositionActionDefinitions(); + + /** + * Get the disposition action definition + * + * @param id the action definition id + * @return {@link DispositionActionDefinition} disposition action definition + */ + DispositionActionDefinition getDispositionActionDefinition(String id); + + /** + * Get the disposition action definition by the name of the disposition action + * + * @param name disposition action name + * @return {@link DispositionActionDefinition} disposition action definition, null if none + */ + DispositionActionDefinition getDispositionActionDefinitionByName(String name); +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionScheduleImpl.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionScheduleImpl.java new file mode 100644 index 0000000000..242357570b --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionScheduleImpl.java @@ -0,0 +1,185 @@ +/* + * 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.module.org_alfresco_module_rm.disposition; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementServiceRegistry; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +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.namespace.RegexQNamePattern; + +/** + * Disposition instructions implementation + * + * @author Roy Wetherall + */ +public class DispositionScheduleImpl implements DispositionSchedule, + RecordsManagementModel +{ + private NodeService nodeService; + private RecordsManagementServiceRegistry services; + private NodeRef dispositionDefinitionNodeRef; + + private List actions; + private Map actionsById; + + //If name is not the same as node-uuid, then action will be stored here too + //Fix for ALF-2588 + private Map actionsByName; + + /** Map of disposition definitions by disposition action name */ + private Map actionsByDispositionActionName; + + public DispositionScheduleImpl(RecordsManagementServiceRegistry services, NodeService nodeService, NodeRef nodeRef) + { + // TODO check that we have a disposition definition node reference + + this.dispositionDefinitionNodeRef = nodeRef; + this.nodeService = nodeService; + this.services = services; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule#getNodeRef() + */ + public NodeRef getNodeRef() + { + return this.dispositionDefinitionNodeRef; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule#getDispositionAuthority() + */ + public String getDispositionAuthority() + { + return (String)this.nodeService.getProperty(this.dispositionDefinitionNodeRef, PROP_DISPOSITION_AUTHORITY); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule#getDispositionInstructions() + */ + public String getDispositionInstructions() + { + return (String)this.nodeService.getProperty(this.dispositionDefinitionNodeRef, PROP_DISPOSITION_INSTRUCTIONS); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule#isRecordLevelDisposition() + */ + public boolean isRecordLevelDisposition() + { + boolean result = false; + Boolean value = (Boolean)this.nodeService.getProperty(this.dispositionDefinitionNodeRef, PROP_RECORD_LEVEL_DISPOSITION); + if (value != null) + { + result = value.booleanValue(); + } + return result; + } + + /** + * Get disposition action definition + * + * @param id action definition identifier + * @return DispositionActionDefinition disposition action definition + */ + public DispositionActionDefinition getDispositionActionDefinition(String id) + { + if (this.actions == null) + { + getDispositionActionsImpl(); + } + + DispositionActionDefinition actionDef = this.actionsById.get(id); + if (actionDef == null) + { + actionDef = this.actionsByName.get(id); + } + return actionDef; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule#getDispositionActionDefinitionByName(java.lang.String) + */ + @Override + public DispositionActionDefinition getDispositionActionDefinitionByName(String name) + { + if (this.actionsByDispositionActionName == null) + { + getDispositionActionsImpl(); + } + return actionsByDispositionActionName.get(name); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule#getDispositionActionDefinitions() + */ + public List getDispositionActionDefinitions() + { + if (this.actions == null) + { + getDispositionActionsImpl(); + } + + return this.actions; + } + + /** + * Get the disposition actions into the local cache + */ + private void getDispositionActionsImpl() + { + List assocs = this.nodeService.getChildAssocs( + this.dispositionDefinitionNodeRef, + ASSOC_DISPOSITION_ACTION_DEFINITIONS, + RegexQNamePattern.MATCH_ALL); + this.actions = new ArrayList(assocs.size()); + this.actionsById = new HashMap(assocs.size()); + this.actionsByName = new HashMap(assocs.size()); + this.actionsByDispositionActionName = new HashMap(assocs.size()); + int index = 0; + for (ChildAssociationRef assoc : assocs) + { + DispositionActionDefinition da = new DispositionActionDefinitionImpl(services.getRecordsManagementEventService(), services.getRecordsManagementActionService(), nodeService, assoc.getChildRef(), index); + actions.add(da); + actionsById.put(da.getId(), da); + index++; + + String actionNodeName = (String) nodeService.getProperty(assoc.getChildRef(), ContentModel.PROP_NAME); + if (!actionNodeName.equals(da.getId())) + { + //It was imported and now has new ID. Old ID may present in old files. + actionsByName.put(actionNodeName, da); + } + + String actionDefintionName = (String)nodeService.getProperty(assoc.getChildRef(), PROP_DISPOSITION_ACTION_NAME); + if (actionDefintionName != null) + { + actionsByDispositionActionName.put(actionDefintionName, da); + } + } + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionSelectionStrategy.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionSelectionStrategy.java new file mode 100644 index 0000000000..e462725a80 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionSelectionStrategy.java @@ -0,0 +1,170 @@ +/* + * 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.module.org_alfresco_module_rm.disposition; + +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; + +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.service.cmr.repository.NodeRef; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * This class offers the default implementation of a strategy for selection of + * disposition schedule for a record when there is more than one which is applicable. + * An example of where this strategy might be used would be in the case of a record + * which was multiply filed. + * + * @author neilm + */ +public class DispositionSelectionStrategy implements RecordsManagementModel +{ + /** Logger */ + private static Log logger = LogFactory.getLog(DispositionSelectionStrategy.class); + + /** Disposition service */ + private DispositionService dispositionService; + + /** + * Set the disposition service + * + * @param dispositionService disposition service + */ + public void setDispositionService(DispositionService dispositionService) + { + this.dispositionService = dispositionService; + } + + /** + * Select the disposition schedule to use given there is more than one + * + * @param recordFolders + * @return + */ + public NodeRef selectDispositionScheduleFrom(List recordFolders) + { + if (recordFolders == null || recordFolders.isEmpty()) + { + return null; + } + else + { + // 46 CHAPTER 2 + // Records assigned more than 1 disposition must be retained and linked to the record folder (category) with the longest + // retention period. + + // Assumption: an event-based disposition action has a longer retention + // period than a time-based one - as we cannot know when an event will occur + // TODO Automatic events? + + SortedSet sortedFolders = new TreeSet(new DispositionableNodeRefComparator()); + for (NodeRef f : recordFolders) + { + sortedFolders.add(f); + } + DispositionSchedule dispSchedule = dispositionService.getDispositionSchedule(sortedFolders.first()); + + if (logger.isDebugEnabled()) + { + logger.debug("Selected disposition schedule: " + dispSchedule); + } + + NodeRef result = null; + if (dispSchedule != null) + { + result = dispSchedule.getNodeRef(); + } + return result; + } + } + + /** + * This class defines a natural comparison order between NodeRefs that have + * the dispositionLifecycle aspect applied. + * This order has the following meaning: NodeRefs with a 'lesser' value are considered + * to have a shorter retention period, although the actual retention period may + * not be straightforwardly determined in all cases. + */ + class DispositionableNodeRefComparator implements Comparator + { + public int compare(final NodeRef f1, final NodeRef f2) + { + // Run as admin user + return AuthenticationUtil.runAs(new RunAsWork() + { + public Integer doWork() throws Exception + { + return new Integer(compareImpl(f1, f2)); + } + + }, AuthenticationUtil.getAdminUserName()).intValue(); + } + + private int compareImpl(NodeRef f1, NodeRef f2) + { + //TODO Check the nodeRefs have the correct aspect + + DispositionAction da1 = dispositionService.getNextDispositionAction(f1); + DispositionAction da2 = dispositionService.getNextDispositionAction(f2); + + if (da1 != null && da2 != null) + { + Date asOfDate1 = da1.getAsOfDate(); + Date asOfDate2 = da2.getAsOfDate(); + // If both record(Folder)s have asOfDates, then use these to compare + if (asOfDate1 != null && asOfDate2 != null) + { + return asOfDate1.compareTo(asOfDate2); + } + // If one has a date and the other doesn't, the one with the date is "less". + // (Defined date is 'shorter' than undefined date as an undefined date means it may be retained forever - theoretically) + else if (asOfDate1 != null || asOfDate2 != null) + { + return asOfDate1 == null ? +1 : -1; + } + else + { + // Neither has an asOfDate. (Somewhat arbitrarily) we'll use the number of events to compare now. + DispositionActionDefinition dad1 = da1.getDispositionActionDefinition(); + DispositionActionDefinition dad2 = da2.getDispositionActionDefinition(); + int eventsCount1 = 0; + int eventsCount2 = 0; + + if (dad1 != null) + { + eventsCount1 = dad1.getEvents().size(); + } + if (dad2 != null) + { + eventsCount2 = dad2.getEvents().size(); + } + return new Integer(eventsCount1).compareTo(eventsCount2); + } + } + + return 0; + } + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionService.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionService.java new file mode 100644 index 0000000000..0d02d2a336 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionService.java @@ -0,0 +1,188 @@ +/* + * 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.module.org_alfresco_module_rm.disposition; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + +/** + * Disposition service + * + * @author Roy Wetherall + * @since 2.0 + */ +public interface DispositionService +{ + /** ========= Disposition Schedule Methods ========= */ + + /** + * Get the disposition schedule for a given record management node. Traverses the hierarchy to + * find the first disposition schedule in the primary hierarchy. + * + * @param nodeRef node reference to record category, record folder or record + * @return {@link DispositionSchedule} disposition schedule + */ + DispositionSchedule getDispositionSchedule(NodeRef nodeRef); + + // Gets all the disposition schedules, not just the first in the primary parent path. + // TODO List getAllDispositionSchedules(NodeRef nodeRef); + + /** + * Get the disposition schedule directly associated with the node specified. Returns + * null if none. + * + * @param nodeRef node reference + * @return {@link DispositionSchedule} disposition schedule directly associated with the node reference, null if none + */ + DispositionSchedule getAssociatedDispositionSchedule(NodeRef nodeRef); + + /** + * Gets the records management container that is directly associated with the disposition schedule. + * + * @param dispositionSchedule disposition schedule + * @return {@link NodeRef} node reference of the associated container + */ + NodeRef getAssociatedRecordsManagementContainer(DispositionSchedule dispositionSchedule); + + /** + * Indicates whether a disposition schedule has any disposable items under its management + * + * @param dispositionSchdule disposition schedule + * @return boolean true if there are disposable items being managed by, false otherwise + */ + boolean hasDisposableItems(DispositionSchedule dispositionSchdule); + + /** + * Gets a list of all the disposable items (records, record folders) that are under the control of + * the disposition schedule. + * + * @param dispositionSchedule disposition schedule + * @return {@link List}<{@link NodeRef}> list of disposable items + */ + List getDisposableItems(DispositionSchedule dispositionSchedule); + + /** + * Indicates whether the node is a disposable item or not (ie is under the control of a disposition schedule) + * + * @param nodeRef node reference + * @return boolean true if node is a disposable item, false otherwise + */ + boolean isDisposableItem(NodeRef nodeRef); + + /** + * Creates a disposition schedule on the given record category. + * + * @param recordCategory + * @param props + * @return {@link DispositionSchedule} + */ + DispositionSchedule createDispositionSchedule(NodeRef recordCategory, Map props); + + // TODO DispositionSchedule updateDispositionSchedule(DispositionScedule, Map props) + + // TODO void removeDispositionSchedule(NodeRef nodeRef); - can only remove if no disposition items + + /** ========= Disposition Action Definition Methods ========= */ + + /** + * Adds a new disposition action definition to the given disposition schedule. + * + * @param schedule The DispositionSchedule to add to + * @param actionDefinitionParams Map of parameters to use to create the action definition + */ + DispositionActionDefinition addDispositionActionDefinition( + DispositionSchedule schedule, + Map actionDefinitionParams); + + /** + * Removes the given disposition action definition from the given disposition + * schedule. + * + * @param schedule The DispositionSchedule to remove from + * @param actionDefinition The DispositionActionDefinition to remove + */ + void removeDispositionActionDefinition( + DispositionSchedule schedule, + DispositionActionDefinition actionDefinition); + + /** + * Updates the given disposition action definition belonging to the given disposition + * schedule. + * + * @param actionDefinition The DispositionActionDefinition to update + * @param actionDefinitionParams Map of parameters to use to update the action definition + * @return The updated DispositionActionDefinition + */ + DispositionActionDefinition updateDispositionActionDefinition( + DispositionActionDefinition actionDefinition, + Map actionDefinitionParams); + + + /** + * TODO MOVE THIS FROM THIS API + * + * @param nodeRef + * @return + */ + boolean isNextDispositionActionEligible(NodeRef nodeRef); + + /** ========= Disposition Action Methods ========= */ + + /** + * Gets the next disposition action for a given node + * + * @param nodeRef + * @return + */ + DispositionAction getNextDispositionAction(NodeRef nodeRef); + + + /** ========= Disposition Action History Methods ========= */ + + /** + * Gets a list of all the completed disposition action in the order they occured. + * + * @param nodeRef record/record folder + * @return List list of completed disposition actions + */ + List getCompletedDispositionActions(NodeRef nodeRef); + + /** + * Helper method to get the last completed disposition action. Returns null + * if there is none. + * + * @param nodeRef record/record folder + * @return DispositionAction last completed disposition action, null if none + */ + DispositionAction getLastCompletedDispostionAction(NodeRef nodeRef); + + /** ========= ========= */ + + /** + * Returns the list of disposition period properties + * + * @return list of disposition period properties + */ + List getDispositionPeriodProperties(); + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionServiceImpl.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionServiceImpl.java new file mode 100644 index 0000000000..baddd47d32 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionServiceImpl.java @@ -0,0 +1,626 @@ +/* + * 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.module.org_alfresco_module_rm.disposition; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementServiceRegistry; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.repo.policy.BehaviourFilter; +import org.alfresco.service.cmr.dictionary.DictionaryService; +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.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.alfresco.util.ParameterCheck; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; + +/** + * Disposition service implementation. + * + * @author Roy Wetherall + */ +public class DispositionServiceImpl implements DispositionService, RecordsManagementModel, ApplicationContextAware +{ + /** Logger */ + private static Log logger = LogFactory.getLog(DispositionServiceImpl.class); + + /** Node service */ + private NodeService nodeService; + + /** Dictionary service */ + private DictionaryService dictionaryService; + + /** Behaviour filter */ + private BehaviourFilter behaviourFilter; + + /** Records management service */ + private RecordsManagementService rmService; + + /** Records management service registry */ + private RecordsManagementServiceRegistry serviceRegistry; + + /** Disposition selection strategy */ + private DispositionSelectionStrategy dispositionSelectionStrategy; + + /** Application context */ + private ApplicationContext applicationContext; + + /** + * Set node service + * + * @param nodeService the node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Set the dictionary service + * + * @param dictionaryServic the dictionary service + */ + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + /** + * Set the behaviour filter. + * + * @param behaviourFilter the behaviour filter + */ + public void setBehaviourFilter(BehaviourFilter behaviourFilter) + { + this.behaviourFilter = behaviourFilter; + } + + /** + * Set the records management service registry + * + * @param serviceRegistry records management registry service + */ + public void setRecordsManagementServiceRegistry(RecordsManagementServiceRegistry serviceRegistry) + { + this.serviceRegistry = serviceRegistry; + } + + /** + * Get the records management service + * NOTE: have to pull it out of the app context manually to prevent Spring circular dependancy issue + * + * @return + */ + public RecordsManagementService getRmService() + { + if (rmService == null) + { + rmService = (RecordsManagementService)applicationContext.getBean("recordsManagementService"); + } + return rmService; + } + + /** + * @see org.springframework.context.ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext) + */ + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException + { + this.applicationContext = applicationContext; + } + + /** + * Set the dispositionSelectionStrategy bean. + * + * @param dispositionSelectionStrategy + */ + public void setDispositionSelectionStrategy(DispositionSelectionStrategy dispositionSelectionStrategy) + { + this.dispositionSelectionStrategy = dispositionSelectionStrategy; + } + + /** ========= Disposition Schedule Methods ========= */ + + /** + * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService#getDispositionSchedule(org.alfresco.service.cmr.repository.NodeRef) + */ + public DispositionSchedule getDispositionSchedule(NodeRef nodeRef) + { + DispositionSchedule di = null; + NodeRef diNodeRef = null; + if (getRmService().isRecord(nodeRef) == true) + { + // Get the record folders for the record + List recordFolders = getRmService().getRecordFolders(nodeRef); + // At this point, we may have disposition instruction objects from 1..n folders. + diNodeRef = dispositionSelectionStrategy.selectDispositionScheduleFrom(recordFolders); + } + else + { + // Get the disposition instructions for the node reference provided + diNodeRef = getDispositionScheduleImpl(nodeRef); + } + + if (diNodeRef != null) + { + di = new DispositionScheduleImpl(serviceRegistry, nodeService, diNodeRef); + } + + return di; + } + + /** + * This method returns a NodeRef + * Gets the disposition instructions + * + * @param nodeRef + * @return + */ + private NodeRef getDispositionScheduleImpl(NodeRef nodeRef) + { + NodeRef result = getAssociatedDispositionScheduleImpl(nodeRef); + + if (result == null) + { + NodeRef parent = this.nodeService.getPrimaryParent(nodeRef).getParentRef(); + if (parent != null && getRmService().isRecordCategory(parent) == true) + { + result = getDispositionScheduleImpl(parent); + } + } + return result; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService#getAssociatedDispositionSchedule(org.alfresco.service.cmr.repository.NodeRef) + */ + public DispositionSchedule getAssociatedDispositionSchedule(NodeRef nodeRef) + { + DispositionSchedule ds = null; + + // Check the noderef parameter + ParameterCheck.mandatory("nodeRef", nodeRef); + if (nodeService.exists(nodeRef) == true) + { + // Get the associated disposition schedule node reference + NodeRef dsNodeRef = getAssociatedDispositionScheduleImpl(nodeRef); + if (dsNodeRef != null) + { + // Cerate disposition schedule object + ds = new DispositionScheduleImpl(serviceRegistry, nodeService, dsNodeRef); + } + } + + return ds; + } + + /** + * Gets the node reference of the disposition schedule associated with the container. + * + * @param nodeRef node reference of the container + * @return {@link NodeRef} node reference of the disposition schedule, null if none + */ + private NodeRef getAssociatedDispositionScheduleImpl(NodeRef nodeRef) + { + NodeRef result = null; + ParameterCheck.mandatory("nodeRef", nodeRef); + + // Make sure we are dealing with an RM node + if (getRmService().isFilePlanComponent(nodeRef) == false) + { + throw new AlfrescoRuntimeException("Can not find the associated disposition schedule for a non records management component. (nodeRef=" + nodeRef.toString() + ")"); + } + + if (this.nodeService.hasAspect(nodeRef, ASPECT_SCHEDULED) == true) + { + List childAssocs = this.nodeService.getChildAssocs(nodeRef, ASSOC_DISPOSITION_SCHEDULE, RegexQNamePattern.MATCH_ALL); + if (childAssocs.size() != 0) + { + ChildAssociationRef firstChildAssocRef = childAssocs.get(0); + result = firstChildAssocRef.getChildRef(); + } + } + + return result; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService#getAssociatedRecordsManagementContainer(org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule) + */ + @Override + public NodeRef getAssociatedRecordsManagementContainer(DispositionSchedule dispositionSchedule) + { + ParameterCheck.mandatory("dispositionSchedule", dispositionSchedule); + NodeRef result = null; + + NodeRef dsNodeRef = dispositionSchedule.getNodeRef(); + if (nodeService.exists(dsNodeRef) == true) + { + List assocs = this.nodeService.getParentAssocs(dsNodeRef, ASSOC_DISPOSITION_SCHEDULE, RegexQNamePattern.MATCH_ALL); + if (assocs.size() != 0) + { + if (assocs.size() != 1) + { + // TODO in the future we should be able to support disposition schedule reuse, but for now just warn that + // only the first disposition schedule will be considered + if (logger.isWarnEnabled() == true) + { + logger.warn("Disposition schedule has more than one associated records management container. " + + "This is not currently supported so only the first container will be considered. " + + "(dispositionScheduleNodeRef=" + dispositionSchedule.getNodeRef().toString() + ")"); + } + } + + // Get the container reference + ChildAssociationRef assoc = assocs.get(0); + result = assoc.getParentRef(); + } + } + + return result; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService#hasDisposableItems(org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule) + */ + @Override + public boolean hasDisposableItems(DispositionSchedule dispositionSchdule) + { + return !getDisposableItems(dispositionSchdule).isEmpty(); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService#getDisposableItems(org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule) + */ + public List getDisposableItems(DispositionSchedule dispositionSchedule) + { + ParameterCheck.mandatory("dispositionSchedule", dispositionSchedule); + + // Get the associated container + NodeRef rmContainer = getAssociatedRecordsManagementContainer(dispositionSchedule); + + // Return the disposable items + return getDisposableItemsImpl(dispositionSchedule.isRecordLevelDisposition(), rmContainer); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService#isDisposableItem(org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + public boolean isDisposableItem(NodeRef nodeRef) + { + return nodeService.hasAspect(nodeRef, ASPECT_DISPOSITION_LIFECYCLE); + } + + /** + * + * @param isRecordLevelDisposition + * @param rmContainer + * @param root + * @return + */ + private List getDisposableItemsImpl(boolean isRecordLevelDisposition, NodeRef rmContainer) + { + List items = getRmService().getAllContained(rmContainer); + List result = new ArrayList(items.size()); + for (NodeRef item : items) + { + if (getRmService().isRecordFolder(item) == true) + { + if (isRecordLevelDisposition == true) + { + result.addAll(getRmService().getRecords(item)); + } + else + { + result.add(item); + } + } + else if (getRmService().isRecordCategory(item) == true) + { + if (getAssociatedDispositionScheduleImpl(item) == null) + { + result.addAll(getDisposableItemsImpl(isRecordLevelDisposition, item)); + } + } + } + return result; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService#createDispositionSchedule(org.alfresco.service.cmr.repository.NodeRef, java.util.Map) + */ + @Override + public DispositionSchedule createDispositionSchedule(NodeRef nodeRef, Map props) + { + NodeRef dsNodeRef = null; + + // Check mandatory parameters + ParameterCheck.mandatory("nodeRef", nodeRef); + + // Check exists + if (nodeService.exists(nodeRef) == false) + { + throw new AlfrescoRuntimeException("Unable to create disposition schedule, because node does not exist. (nodeRef=" + nodeRef.toString() + ")"); + } + + // Check is sub-type of rm:recordCategory + QName nodeRefType = nodeService.getType(nodeRef); + if (TYPE_RECORD_CATEGORY.equals(nodeRefType) == false && + dictionaryService.isSubClass(nodeRefType, TYPE_RECORD_CATEGORY) == false) + { + throw new AlfrescoRuntimeException("Unable to create disposition schedule on a node that is not a records management container."); + } + + behaviourFilter.disableBehaviour(nodeRef, ASPECT_SCHEDULED); + try + { + // Add the schedules aspect if required + if (nodeService.hasAspect(nodeRef, ASPECT_SCHEDULED) == false) + { + nodeService.addAspect(nodeRef, ASPECT_SCHEDULED, null); + } + + // Check whether there is already a disposition schedule object present + List assocs = nodeService.getChildAssocs(nodeRef, ASSOC_DISPOSITION_SCHEDULE, RegexQNamePattern.MATCH_ALL); + if (assocs.size() == 0) + { + DispositionSchedule currentDispositionSchdule = getDispositionSchedule(nodeRef); + if (currentDispositionSchdule != null) + { + List items = getDisposableItemsImpl(currentDispositionSchdule.isRecordLevelDisposition(), nodeRef); + if (items.size() != 0) + { + throw new AlfrescoRuntimeException("Can not create a disposition schedule if there are disposable items already under the control of an other disposition schedule"); + } + } + + // Create the disposition schedule object + dsNodeRef = nodeService.createNode( + nodeRef, + ASSOC_DISPOSITION_SCHEDULE, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, QName.createValidLocalName("dispositionSchedule")), + TYPE_DISPOSITION_SCHEDULE, + props).getChildRef(); + } + else + { + // Error since the node already has a disposition schedule set + throw new AlfrescoRuntimeException("Unable to create disposition schedule on node that already has a disposition schedule."); + } + } + finally + { + behaviourFilter.enableBehaviour(nodeRef, ASPECT_SCHEDULED); + } + + // Create the return object + return new DispositionScheduleImpl(serviceRegistry, nodeService, dsNodeRef); + } + + /** ========= Disposition Action Definition Methods ========= */ + + /** + * + */ + public DispositionActionDefinition addDispositionActionDefinition( + DispositionSchedule schedule, + Map actionDefinitionParams) + { + // make sure at least a name has been defined + String name = (String)actionDefinitionParams.get(PROP_DISPOSITION_ACTION_NAME); + if (name == null || name.length() == 0) + { + throw new IllegalArgumentException("'name' parameter is mandatory when creating a disposition action definition"); + } + + // TODO: also check the action name is valid? + + // create the child association from the schedule to the action definition + NodeRef actionNodeRef = this.nodeService.createNode(schedule.getNodeRef(), + RecordsManagementModel.ASSOC_DISPOSITION_ACTION_DEFINITIONS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, + QName.createValidLocalName(name)), + RecordsManagementModel.TYPE_DISPOSITION_ACTION_DEFINITION, actionDefinitionParams).getChildRef(); + + // get the updated disposition schedule and retrieve the new action definition + NodeRef scheduleParent = this.nodeService.getPrimaryParent(schedule.getNodeRef()).getParentRef(); + DispositionSchedule updatedSchedule = this.getDispositionSchedule(scheduleParent); + return updatedSchedule.getDispositionActionDefinition(actionNodeRef.getId()); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService#removeDispositionActionDefinition(org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule, org.alfresco.module.org_alfresco_module_rm.disposition.DispositionActionDefinition) + */ + public void removeDispositionActionDefinition(DispositionSchedule schedule, DispositionActionDefinition actionDefinition) + { + // check first whether action definitions can be removed + if (hasDisposableItems(schedule) == true) + { + throw new AlfrescoRuntimeException("Can not remove action definitions from schedule '" + + schedule.getNodeRef() + "' as one or more record or record folders are present."); + } + + // remove the child node representing the action definition + this.nodeService.removeChild(schedule.getNodeRef(), actionDefinition.getNodeRef()); + } + + /** + * Updates the given disposition action definition belonging to the given disposition + * schedule. + * + * @param schedule The DispositionSchedule the action belongs to + * @param actionDefinition The DispositionActionDefinition to update + * @param actionDefinitionParams Map of parameters to use to update the action definition + * @return The updated DispositionActionDefinition + */ + public DispositionActionDefinition updateDispositionActionDefinition( + DispositionActionDefinition actionDefinition, + Map actionDefinitionParams) + { + // update the node with properties + this.nodeService.addProperties(actionDefinition.getNodeRef(), actionDefinitionParams); + + // get the updated disposition schedule and retrieve the updated action definition + NodeRef ds = this.nodeService.getPrimaryParent(actionDefinition.getNodeRef()).getParentRef(); + DispositionSchedule updatedSchedule = new DispositionScheduleImpl(serviceRegistry, nodeService, ds); + return updatedSchedule.getDispositionActionDefinition(actionDefinition.getId()); + } + + /** + * + */ + public boolean isNextDispositionActionEligible(NodeRef nodeRef) + { + boolean result = false; + + // Get the disposition instructions + DispositionSchedule di = getDispositionSchedule(nodeRef); + NodeRef nextDa = getNextDispositionActionNodeRef(nodeRef); + if (di != null && + this.nodeService.hasAspect(nodeRef, ASPECT_DISPOSITION_LIFECYCLE) == true && + nextDa != null) + { + // If it has an asOf date and it is greater than now the action is eligible + Date asOf = (Date)this.nodeService.getProperty(nextDa, PROP_DISPOSITION_AS_OF); + if (asOf != null && + asOf.before(new Date()) == true) + { + result = true; + } + + if (result == false) + { + // If all the events specified on the action have been completed the action is eligible + List assocs = this.nodeService.getChildAssocs(nextDa, ASSOC_EVENT_EXECUTIONS, RegexQNamePattern.MATCH_ALL); + for (ChildAssociationRef assoc : assocs) + { + NodeRef eventExecution = assoc.getChildRef(); + Boolean isCompleteValue = (Boolean)this.nodeService.getProperty(eventExecution, PROP_EVENT_EXECUTION_COMPLETE); + boolean isComplete = false; + if (isCompleteValue != null) + { + isComplete = isCompleteValue.booleanValue(); + + // TODO this only works for the OR use case .. need to handle optional AND handling + if (isComplete == true) + { + result = true; + break; + } + } + } + } + } + + return result; + } + + /** + * Get the next disposition action node. Null if none present. + * + * @param nodeRef the disposable node reference + * @return NodeRef the next disposition action, null if none + */ + private NodeRef getNextDispositionActionNodeRef(NodeRef nodeRef) + { + NodeRef result = null; + List assocs = this.nodeService.getChildAssocs(nodeRef, ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL); + if (assocs.size() != 0) + { + result = assocs.get(0).getChildRef(); + } + return result; + } + + /** ========= Disposition Action Methods ========= */ + + /** + * + */ + public DispositionAction getNextDispositionAction(NodeRef nodeRef) + { + DispositionAction result = null; + NodeRef dispositionActionNodeRef = getNextDispositionActionNodeRef(nodeRef); + + if (dispositionActionNodeRef != null) + { + result = new DispositionActionImpl(this.serviceRegistry, dispositionActionNodeRef); + } + return result; + } + + + /** ========= Disposition Action History Methods ========= */ + + public List getCompletedDispositionActions(NodeRef nodeRef) + { + List assocs = this.nodeService.getChildAssocs(nodeRef, ASSOC_DISPOSITION_ACTION_HISTORY, RegexQNamePattern.MATCH_ALL); + List result = new ArrayList(assocs.size()); + for (ChildAssociationRef assoc : assocs) + { + NodeRef dispositionActionNodeRef = assoc.getChildRef(); + result.add(new DispositionActionImpl(serviceRegistry, dispositionActionNodeRef)); + } + return result; + } + + public DispositionAction getLastCompletedDispostionAction(NodeRef nodeRef) + { + DispositionAction result = null; + List list = getCompletedDispositionActions(nodeRef); + if (list.isEmpty() == false) + { + // Get the last disposition action in the list + result = list.get(list.size()-1); + } + return result; + } + + + public List getDispositionPeriodProperties() + { + DispositionPeriodProperties dpp = (DispositionPeriodProperties)applicationContext.getBean(DispositionPeriodProperties.BEAN_NAME); + + if (dpp == null) + { + return Collections.emptyList(); + } + else + { + return dpp.getPeriodProperties(); + } + } + + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/dod5015/DOD5015Model.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/dod5015/DOD5015Model.java new file mode 100644 index 0000000000..3b9420ca1c --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/dod5015/DOD5015Model.java @@ -0,0 +1,78 @@ +/* + * 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.module.org_alfresco_module_rm.dod5015; + +import org.alfresco.service.namespace.QName; + + +/** + * Helper class containing DOD 5015 model qualified names + * + * @author Roy Wetherall + */ +public interface DOD5015Model +{ + // Namespace details + public static final String DOD_URI = "http://www.alfresco.org/model/dod5015/1.0"; + public static final String DOD_PREFIX = "dod"; + + // Record series DOD type + public static final QName TYPE_RECORD_SERIES = QName.createQName(DOD_URI, "recordSeries"); + + // DOD 5015 Custom Type aspects and their properties + // Scanned Record + public static final QName ASPECT_SCANNED_RECORD = QName.createQName(DOD_URI, "scannedRecord"); + public static final QName PROP_SCANNED_FORMAT = QName.createQName(DOD_URI, "scannedFormat"); + public static final QName PROP_SCANNED_FORMAT_VERSION = QName.createQName(DOD_URI, "scannedFormatVersion"); + public static final QName PROP_RESOLUTION_X = QName.createQName(DOD_URI, "resolutionX"); + public static final QName PROP_RESOLUTION_Y = QName.createQName(DOD_URI, "resolutionY"); + public static final QName PROP_SCANNED_BIT_DEPTH = QName.createQName(DOD_URI, "scannedBitDepth"); + + // PDF Record + public static final QName ASPECT_PDF_RECORD = QName.createQName(DOD_URI, "pdfRecord"); + public static final QName PROP_PRODUCING_APPLICATION = QName.createQName(DOD_URI, "producingApplication"); + public static final QName PROP_PRODUCING_APPLICATION_VERSION = QName.createQName(DOD_URI, "producingApplicationVersion"); + public static final QName PROP_PDF_VERSION = QName.createQName(DOD_URI, "pdfVersion"); + public static final QName PROP_CREATING_APPLICATION = QName.createQName(DOD_URI, "creatingApplication"); + public static final QName PROP_DOCUMENT_SECURITY_SETTINGS = QName.createQName(DOD_URI, "documentSecuritySettings"); + + // Digital Photograph Record + public static final QName ASPECT_DIGITAL_PHOTOGRAPH_RECORD = QName.createQName(DOD_URI, "digitalPhotographRecord"); + public static final QName PROP_CAPTION = QName.createQName(DOD_URI, "caption"); + public static final QName PROP_PHOTOGRAPHER = QName.createQName(DOD_URI, "photographer"); + public static final QName PROP_COPYRIGHT = QName.createQName(DOD_URI, "copyright"); + public static final QName PROP_BIT_DEPTH = QName.createQName(DOD_URI, "bitDepth"); + public static final QName PROP_IMAGE_SIZE_X = QName.createQName(DOD_URI, "imageSizeX"); + public static final QName PROP_IMAGE_SIZE_Y = QName.createQName(DOD_URI, "imageSizeY"); + public static final QName PROP_IMAGE_SOURCE = QName.createQName(DOD_URI, "imageSource"); + public static final QName PROP_COMPRESSION = QName.createQName(DOD_URI, "compression"); + public static final QName PROP_ICC_ICM_PROFILE = QName.createQName(DOD_URI, "iccIcmProfile"); + public static final QName PROP_EXIF_INFORMATION = QName.createQName(DOD_URI, "exifInformation"); + + // Web Record + public static final QName ASPECT_WEB_RECORD = QName.createQName(DOD_URI, "webRecord"); + public static final QName PROP_WEB_FILE_NAME = QName.createQName(DOD_URI, "webFileName"); + public static final QName PROP_WEB_PLATFORM = QName.createQName(DOD_URI, "webPlatform"); + public static final QName PROP_WEBSITE_NAME = QName.createQName(DOD_URI, "webSiteName"); + public static final QName PROP_WEB_SITE_URL = QName.createQName(DOD_URI, "webSiteURL"); + public static final QName PROP_CAPTURE_METHOD = QName.createQName(DOD_URI, "captureMethod"); + public static final QName PROP_CAPTURE_DATE = QName.createQName(DOD_URI, "captureDate"); + public static final QName PROP_CONTACT = QName.createQName(DOD_URI, "contact"); + public static final QName PROP_CONTENT_MANAGEMENT_SYSTEM = QName.createQName(DOD_URI, "contentManagementSystem"); +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/email/CustomEmailMappingService.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/email/CustomEmailMappingService.java new file mode 100644 index 0000000000..a64df4f860 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/email/CustomEmailMappingService.java @@ -0,0 +1,32 @@ +/* + * 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.module.org_alfresco_module_rm.email; + +import java.util.Set; + +public interface CustomEmailMappingService +{ + public Set getCustomMappings(); + + public void addCustomMapping(String from, String to); + + public void deleteCustomMapping(String from, String to); + + public void init(); +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/email/CustomEmailMappingServiceImpl.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/email/CustomEmailMappingServiceImpl.java new file mode 100644 index 0000000000..9b4be07048 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/email/CustomEmailMappingServiceImpl.java @@ -0,0 +1,411 @@ +/* + * 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.module.org_alfresco_module_rm.email; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.repo.content.ContentServicePolicies; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.content.metadata.RFC822MetadataExtracter; +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +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; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.NamespacePrefixResolver; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; + +public class CustomEmailMappingServiceImpl implements CustomEmailMappingService +{ + + private RFC822MetadataExtracter extracter; + private NodeService nodeService; + private NamespacePrefixResolver nspr; + private PolicyComponent policyComponent; + private ContentService contentService; + private TransactionService transactionService; + + private Set customMappings = Collections.synchronizedSet(new HashSet()); + + private static Log logger = LogFactory.getLog(CustomEmailMappingServiceImpl.class); + + + public void setPolicyComponent(PolicyComponent policyComponent) + { + this.policyComponent = policyComponent; + } + + /** + * Get the name space prefix resolver + * @return the name space prefix resolver + */ + public NamespacePrefixResolver getNamespacePrefixResolver() + { + return nspr; + } + + /** + * Set the name space prefix resolver + * @param nspr + */ + public void setNamespacePrefixResolver(NamespacePrefixResolver nspr) + { + this.nspr = nspr; + } + + /** + * + */ + public void init() + { + CustomMapping[] rmHardCodedMappings = { + new CustomMapping("Date", "rma:dateReceived"), + new CustomMapping("messageTo", "rma:address"), + new CustomMapping("messageFrom", "rma:originator"), + new CustomMapping("messageSent", "rma:publicationDate"), + new CustomMapping("messageCc", "rma:otherAddress") + }; + + NodeRef configNode = getConfigNode(); + if(configNode != null) + { + /** + * Get any custom mappings. + */ + customMappings = readConfig(configNode); + } + + /** + * ensure that the customMappings contain the RM specific mappings + */ + for(CustomMapping mapping : rmHardCodedMappings) + { + if(!customMappings.contains(mapping)) + { + customMappings.add(mapping); + } + } + + // Get the read only existing configuration + Map> currentMapping = extracter.getCurrentMapping(); + + Map> newMapping = new HashMap>(17); + newMapping.putAll(currentMapping); + + for(CustomMapping mapping : customMappings) + { + QName newQName = QName.createQName(mapping.getTo(), nspr); + Set values = newMapping.get(mapping.getFrom()); + if(values == null) + { + values = new HashSet(); + newMapping.put(mapping.getFrom(), values); + } + values.add(newQName); + } + + // Now update the metadata extracter + extracter.setMapping(newMapping); + + // Register interest in the onContentUpdate policy + policyComponent.bindClassBehaviour( + ContentServicePolicies.OnContentUpdatePolicy.QNAME, + RecordsManagementModel.TYPE_EMAIL_CONFIG, + new JavaBehaviour(this, "onContentUpdate")); + + } + + public void onContentUpdate(NodeRef nodeRef, boolean newContent) + { + NodeRef configNode = getConfigNode(); + if(configNode != null) + { + Set newMappings = readConfig(configNode); + + customMappings.addAll(newMappings); + + for(CustomMapping mapping : customMappings) + { + if(!newMappings.contains(mapping)) + { + customMappings.remove(mapping); + } + } + } + } + + public void beforeDeleteNode(NodeRef nodeRef) + { + } + + public void onCreateNode(ChildAssociationRef childAssocRef) + { + + } + + public Set getCustomMappings() + { + // add all the lists data to a Map + Set emailMap = new HashSet(); + + Map> currentMapping = extracter.getCurrentMapping(); + + for(String key : currentMapping.keySet()) + { + Set set = currentMapping.get(key); + + for(QName qname : set) + { + CustomMapping value = new CustomMapping(); + value.setFrom(key); + QName resolvedQname = qname.getPrefixedQName(nspr); + value.setTo(resolvedQname.toPrefixString()); + emailMap.add(value); + } + } + + return emailMap; + } + + + public void addCustomMapping(String from, String to) + { + // Get the read only existing configuration + Map> currentMapping = extracter.getCurrentMapping(); + + Map> newMapping = new HashMap>(17); + newMapping.putAll(currentMapping); + + QName newQName = QName.createQName(to, nspr); + + Set values = newMapping.get(from); + if(values == null) + { + values = new HashSet(); + newMapping.put(from, values); + } + values.add(newQName); + + CustomMapping xxx = new CustomMapping(); + xxx.setFrom(from); + xxx.setTo(to); + customMappings.add(xxx); + + updateOrCreateEmailConfig(customMappings); + + // Crash in the new config. + extracter.setMapping(newMapping); + } + + public void deleteCustomMapping(String from, String to) + { + // Get the read only existing configuration + Map> currentMapping = extracter.getCurrentMapping(); + + Map> newMapping = new HashMap>(17); + newMapping.putAll(currentMapping); + + QName oldQName = QName.createQName(to, nspr); + + Set values = newMapping.get(from); + if(values != null) + { + values.remove(oldQName); + } + + CustomMapping toDelete = new CustomMapping(from, to); + customMappings.remove(toDelete); + + updateOrCreateEmailConfig(customMappings); + + // Crash in the new config. + extracter.setMapping(newMapping); + } + + public void setExtracter(RFC822MetadataExtracter extractor) + { + this.extracter = extractor; + } + + public RFC822MetadataExtracter getExtracter() + { + return extracter; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public NodeService getNodeService() + { + return nodeService; + } + + // Default + private StoreRef storeRef = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "SpacesStore"); + + private static final String CONFIG_NAME = "imapConfig.json"; + + /** + * + * @param nodeRef + * @return + */ + private Set readConfig(NodeRef nodeRef) + { + Set newMappings = new HashSet(); + + ContentReader cr = contentService.getReader(nodeRef, ContentModel.PROP_CONTENT); + if (cr != null) + { + String text = cr.getContentString(); + + try + { + JSONArray jsonArray = new JSONArray(new JSONTokener(text)); + for(int i = 0 ; i < jsonArray.length(); i++) + { + JSONObject obj = jsonArray.getJSONObject(i); + CustomMapping mapping = new CustomMapping(); + mapping.setFrom(obj.getString("from")); + mapping.setTo(obj.getString("to")); + newMappings.add(mapping); + } + return newMappings; + } + catch (JSONException je) + { + logger.warn("unable to read custom email configuration", je); + return newMappings; + } + + } + return newMappings; + } + + public NodeRef updateOrCreateEmailConfig(Set customMappings) + { + NodeRef caveatConfig = updateOrCreateEmailConfig(); + + try + { + JSONArray mappings = new JSONArray(); + for(CustomMapping mapping : customMappings) + { + JSONObject obj = new JSONObject(); + obj.put("from", mapping.getFrom()); + obj.put("to", mapping.getTo()); + mappings.put(obj); + } + + // Update the content + ContentWriter writer = this.contentService.getWriter(caveatConfig, ContentModel.PROP_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.setEncoding("UTF-8"); + writer.putContent(mappings.toString()); + } + catch (JSONException je) + { + + } + + + return caveatConfig; + } + + public NodeRef updateOrCreateEmailConfig(String txt) + { + NodeRef caveatConfig = updateOrCreateEmailConfig(); + + // Update the content + ContentWriter writer = this.contentService.getWriter(caveatConfig, ContentModel.PROP_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.setEncoding("UTF-8"); + writer.putContent(txt); + + return caveatConfig; + } + + private NodeRef updateOrCreateEmailConfig() + { + NodeRef caveatConfig = getConfigNode(); + if (caveatConfig == null) + { + logger.debug("custom email configuration does not exist - creating new"); + NodeRef rootNode = nodeService.getRootNode(storeRef); + //nodeService.addAspect(rootNode, VersionModel.ASPECT_VERSION_STORE_ROOT, null); + + // Create caveat config + caveatConfig = nodeService.createNode(rootNode, + RecordsManagementModel.ASSOC_EMAIL_CONFIG, + QName.createQName(RecordsManagementModel.RM_URI, CONFIG_NAME), + RecordsManagementModel.TYPE_EMAIL_CONFIG).getChildRef(); + + nodeService.setProperty(caveatConfig, ContentModel.PROP_NAME, CONFIG_NAME); + } + + return caveatConfig; + } + + public NodeRef getConfigNode() + { + NodeRef rootNode = nodeService.getRootNode(storeRef); + return nodeService.getChildByName(rootNode, RecordsManagementModel.ASSOC_EMAIL_CONFIG, CONFIG_NAME); + } + + public void setContentService(ContentService contentService) + { + this.contentService = contentService; + } + + public ContentService getContentService() + { + return contentService; + } + + public void setTransactionService(TransactionService transactionService) + { + this.transactionService = transactionService; + } + + public TransactionService getTransactionService() + { + return transactionService; + } + + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/email/CustomMapping.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/email/CustomMapping.java new file mode 100644 index 0000000000..b9f75d1532 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/email/CustomMapping.java @@ -0,0 +1,96 @@ +/* + * 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.module.org_alfresco_module_rm.email; + + +public class CustomMapping +{ + private String from; + private String to; + + public CustomMapping() + { + + } + + public CustomMapping(String from, String to) + { + this.from = from; + this.to = to; + } + + public void setFrom(String from) + { + this.from = from; + } + + public String getFrom() + { + return from; + } + + public void setTo(String to) + { + this.to = to; + } + + public String getTo() + { + return to; + } + + public int hashCode() + { + if(from != null && to != null) + { + return (from + to).hashCode(); + } + else + { + return 1; + } + } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + + final CustomMapping other = (CustomMapping) obj; + + if (!from.equals(other.getFrom())) + { + return false; + } + if (!to.equals(other.getTo())) + { + return false; + } + return true; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/email/RFC822MetadataExtracter.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/email/RFC822MetadataExtracter.java new file mode 100644 index 0000000000..65f28e6919 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/email/RFC822MetadataExtracter.java @@ -0,0 +1,111 @@ +/* + * 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.module.org_alfresco_module_rm.email; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +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; + +/** + * Extended RFC822 Metadata Extractor that is sensitive to whether we are in a RM + * site or not. + * + * @author Roy Wetherall + */ +public class RFC822MetadataExtracter extends org.alfresco.repo.content.metadata.RFC822MetadataExtracter +{ + /** Reference to default properties */ + private static final String PROPERTIES_URL = "org/alfresco/repo/content/metadata/RFC822MetadataExtracter.properties"; + + /** Node service */ + private NodeService nodeService; + + /** + * Sets the node service + * @param nodeService node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * @see org.alfresco.repo.content.metadata.AbstractMappingMetadataExtracter#filterSystemProperties(java.util.Map, java.util.Map) + */ + @Override + protected void filterSystemProperties(Map systemProperties, Map targetProperties) + { + NodeRef nodeRef = getNodeRef(targetProperties); + if (nodeRef == null || nodeService.hasAspect(nodeRef, RecordsManagementModel.ASPECT_RECORD) == false) + { + // Remove all rm namespace properties from the system map + Map clone = new HashMap(systemProperties); + for (QName propName : clone.keySet()) + { + if (RecordsManagementModel.RM_URI.equals(propName.getNamespaceURI()) == true) + { + systemProperties.remove(propName); + } + } + } + } + + /** + * @see org.alfresco.repo.content.metadata.AbstractMappingMetadataExtracter#getDefaultMapping() + */ + protected Map> getDefaultMapping() + { + // Attempt to load the properties + return readMappingProperties(PROPERTIES_URL); + } + + /** + * Given a set of properties, try and retrieve the node reference + * @param properties node properties + * @return NodeRef null if none, otherwise valid node reference + */ + private NodeRef getNodeRef(Map properties) + { + NodeRef result = null; + + // Get the elements of the node reference + String storeProto = (String)properties.get(ContentModel.PROP_STORE_PROTOCOL); + String storeId = (String)properties.get(ContentModel.PROP_STORE_IDENTIFIER); + String nodeId = (String)properties.get(ContentModel.PROP_NODE_UUID); + + if (storeProto != null && storeProto.length() != 0 && + storeId != null && storeId.length() != 0 && + nodeId != null && nodeId.length() != 0) + + { + // Create the node reference + result = new NodeRef(new StoreRef(storeProto, storeId), nodeId); + } + + return result; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/event/EventCompletionDetails.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/event/EventCompletionDetails.java new file mode 100644 index 0000000000..6641fb1c31 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/event/EventCompletionDetails.java @@ -0,0 +1,162 @@ +/* + * 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.module.org_alfresco_module_rm.event; + +import java.util.Date; + +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Event completion details + * + * @author Roy Wetherall + */ +public class EventCompletionDetails +{ + private NodeRef nodeRef; + private String eventName; + private String eventLabel; + private boolean eventExecutionAutomatic; + private boolean eventComplete; + private Date eventCompletedAt; + private String eventCompletedBy; + + + /** + * @param nodeRef + * @param eventName + * @param eventLabel + * @param eventExecutionAutomatic + * @param eventComplete + * @param eventCompletedAt + * @param eventCompletedBy + */ + public EventCompletionDetails( NodeRef nodeRef, + String eventName, + String eventLabel, + boolean eventExecutionAutomatic, + boolean eventComplete, + Date eventCompletedAt, + String eventCompletedBy) + { + this.nodeRef = nodeRef; + this.eventName = eventName; + this.eventLabel = eventLabel; + this.eventExecutionAutomatic = eventExecutionAutomatic; + this.eventComplete = eventComplete; + this.eventCompletedAt = eventCompletedAt; + this.eventCompletedBy = eventCompletedBy; + } + + /** + * @return the node reference + */ + public NodeRef getNodeRef() + { + return nodeRef; + } + + /** + * @return the eventName + */ + public String getEventName() + { + return eventName; + } + + /** + * @param eventName the eventName to set + */ + public void setEventName(String eventName) + { + this.eventName = eventName; + } + + /** + * @return The display label of the event + */ + public String getEventLabel() + { + return this.eventLabel; + } + + /** + * @return the eventExecutionAutomatic + */ + public boolean isEventExecutionAutomatic() + { + return eventExecutionAutomatic; + } + + /** + * @param eventExecutionAutomatic the eventExecutionAutomatic to set + */ + public void setEventExecutionAutomatic(boolean eventExecutionAutomatic) + { + this.eventExecutionAutomatic = eventExecutionAutomatic; + } + + /** + * @return the eventComplete + */ + public boolean isEventComplete() + { + return eventComplete; + } + + /** + * @param eventComplete the eventComplete to set + */ + public void setEventComplete(boolean eventComplete) + { + this.eventComplete = eventComplete; + } + + /** + * @return the eventCompletedAt + */ + public Date getEventCompletedAt() + { + return eventCompletedAt; + } + + /** + * @param eventCompletedAt the eventCompletedAt to set + */ + public void setEventCompletedAt(Date eventCompletedAt) + { + this.eventCompletedAt = eventCompletedAt; + } + + /** + * @return the eventCompletedBy + */ + public String getEventCompletedBy() + { + return eventCompletedBy; + } + + /** + * @param eventCompletedBy the eventCompletedBy to set + */ + public void setEventCompletedBy(String eventCompletedBy) + { + this.eventCompletedBy = eventCompletedBy; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/event/OnReferenceCreateEventType.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/event/OnReferenceCreateEventType.java new file mode 100644 index 0000000000..58f37ef1a1 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/event/OnReferenceCreateEventType.java @@ -0,0 +1,175 @@ +/* + * 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.module.org_alfresco_module_rm.event; + +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementPolicies; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementPolicies.OnCreateReference; +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService; +import org.alfresco.module.org_alfresco_module_rm.action.impl.CompleteEventAction; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionAction; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.policy.Behaviour.NotificationFrequency; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + +/** + * On reference create event type + * + * @author Roy Wetherall + */ +public class OnReferenceCreateEventType extends SimpleRecordsManagementEventTypeImpl + implements RecordsManagementModel, + OnCreateReference +{ + /** Records management service */ + @SuppressWarnings("unused") + private RecordsManagementService recordsManagementService; + + /** Records management action service */ + private RecordsManagementActionService recordsManagementActionService; + + /** Disposition service */ + private DispositionService dispositionService; + + /** Policy component */ + private PolicyComponent policyComponent; + + /** Reference */ + private QName reference; + + /** + * @param recordsManagementService the records management service to set + */ + public void setRecordsManagementService(RecordsManagementService recordsManagementService) + { + this.recordsManagementService = recordsManagementService; + } + + /** + * @param dispositionService the disposition service to set + */ + public void setDispositionService(DispositionService dispositionService) + { + this.dispositionService = dispositionService; + } + + /** + * @param recordsManagementActionService the recordsManagementActionService to set + */ + public void setRecordsManagementActionService(RecordsManagementActionService recordsManagementActionService) + { + this.recordsManagementActionService = recordsManagementActionService; + } + + /** + * Set policy components + * + * @param policyComponent policy component + */ + public void setPolicyComponent(PolicyComponent policyComponent) + { + this.policyComponent = policyComponent; + } + + /** + * Set the reference + * + * @param reference + */ + public void setReferenceName(String reference) + { + this.reference = QName.createQName(reference); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.event.SimpleRecordsManagementEventTypeImpl#init() + */ + public void init() + { + super.init(); + + // Register interest in the on create reference policy + policyComponent.bindClassBehaviour(RecordsManagementPolicies.ON_CREATE_REFERENCE, + ASPECT_RECORD, + new JavaBehaviour(this, "onCreateReference", NotificationFrequency.TRANSACTION_COMMIT)); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.event.SimpleRecordsManagementEventTypeImpl#isAutomaticEvent() + */ + @Override + public boolean isAutomaticEvent() + { + return true; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementPolicies.OnCreateReference#onCreateReference(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName) + */ + public void onCreateReference(final NodeRef fromNodeRef, final NodeRef toNodeRef, final QName reference) + { + AuthenticationUtil.RunAsWork work = new AuthenticationUtil.RunAsWork() + { + public Object doWork() throws Exception + { + // Check whether it is the reference type we care about + if (reference.equals(OnReferenceCreateEventType.this.reference) == true) + { + DispositionAction da = dispositionService.getNextDispositionAction(toNodeRef); + if (da != null) + { + List events = da.getEventCompletionDetails(); + for (EventCompletionDetails event : events) + { + RecordsManagementEvent rmEvent = recordsManagementEventService.getEvent(event.getEventName()); + if (event.isEventComplete() == false && + rmEvent.getType().equals(getName()) == true) + { + // Complete the event + Map params = new HashMap(3); + params.put(CompleteEventAction.PARAM_EVENT_NAME, event.getEventName()); + params.put(CompleteEventAction.PARAM_EVENT_COMPLETED_BY, AuthenticationUtil.getFullyAuthenticatedUser()); + params.put(CompleteEventAction.PARAM_EVENT_COMPLETED_AT, new Date()); + recordsManagementActionService.executeRecordsManagementAction(toNodeRef, "completeEvent", params); + + break; + } + } + } + } + + return null; + } + }; + + AuthenticationUtil.runAs(work, AuthenticationUtil.getAdminUserName()); + + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/event/OnReferencedRecordActionedUpon.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/event/OnReferencedRecordActionedUpon.java new file mode 100644 index 0000000000..ad8dc7558a --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/event/OnReferencedRecordActionedUpon.java @@ -0,0 +1,256 @@ +/* + * 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.module.org_alfresco_module_rm.event; + +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminService; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementPolicies; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService; +import org.alfresco.module.org_alfresco_module_rm.action.impl.CompleteEventAction; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionAction; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.policy.Behaviour.NotificationFrequency; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.service.cmr.repository.AssociationRef; +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.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; + +/** + * + * + * @author Roy Wetherall + */ +public class OnReferencedRecordActionedUpon extends SimpleRecordsManagementEventTypeImpl + implements RecordsManagementModel + +{ + /** Records management service */ + private RecordsManagementService recordsManagementService; + + /** Disposition service */ + private DispositionService dispositionService; + + /** Records management action service */ + private RecordsManagementActionService recordsManagementActionService; + + /** Records management admin service */ + private RecordsManagementAdminService recordsManagementAdminService; + + /** Node service */ + private NodeService nodeService; + + /** Policy component */ + private PolicyComponent policyComponent; + + /** Action name */ + private String actionName; + + /** Reference */ + private QName reference; + + /** + * @param recordsManagementService the records management service to set + */ + public void setRecordsManagementService(RecordsManagementService recordsManagementService) + { + this.recordsManagementService = recordsManagementService; + } + + /** + * @param dispositionService the disposition service + */ + public void setDispositionService(DispositionService dispositionService) + { + this.dispositionService = dispositionService; + } + + /** + * @param recordsManagementActionService the recordsManagementActionService to set + */ + public void setRecordsManagementActionService(RecordsManagementActionService recordsManagementActionService) + { + this.recordsManagementActionService = recordsManagementActionService; + } + + public void setRecordsManagementAdminService(RecordsManagementAdminService recordsManagementAdminService) + { + this.recordsManagementAdminService = recordsManagementAdminService; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Set policy components + * + * @param policyComponent policy component + */ + public void setPolicyComponent(PolicyComponent policyComponent) + { + this.policyComponent = policyComponent; + } + + /** + * Set the reference + * + * @param reference + */ + public void setReferenceName(String reference) + { + this.reference = QName.createQName(reference); + } + + public void setActionName(String actionName) + { + this.actionName = actionName; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.event.SimpleRecordsManagementEventTypeImpl#init() + */ + public void init() + { + super.init(); + + // Register interest in the on create reference policy + policyComponent.bindClassBehaviour(RecordsManagementPolicies.BEFORE_RM_ACTION_EXECUTION, + ASPECT_FILE_PLAN_COMPONENT, + new JavaBehaviour(this, "beforeActionExecution", NotificationFrequency.FIRST_EVENT)); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.event.SimpleRecordsManagementEventTypeImpl#isAutomaticEvent() + */ + @Override + public boolean isAutomaticEvent() + { + return true; + } + + public void beforeActionExecution(final NodeRef nodeRef, final String name, final Map parameters) + { + AuthenticationUtil.RunAsWork work = new AuthenticationUtil.RunAsWork() + { + public Object doWork() throws Exception + { + if (nodeService.exists(nodeRef) == true) + { + if (name.equals(actionName) == true) + { + QName type = nodeService.getType(nodeRef); + if (TYPE_TRANSFER.equals(type) == true) + { + List assocs = nodeService.getChildAssocs(nodeRef, ASSOC_TRANSFERRED, RegexQNamePattern.MATCH_ALL); + for (ChildAssociationRef assoc : assocs) + { + processRecordFolder(assoc.getChildRef()); + } + } + else + { + processRecordFolder(nodeRef); + } + } + } + + return null; + } + }; + + AuthenticationUtil.runAs(work, AuthenticationUtil.getAdminUserName()); + + } + + private void processRecordFolder(NodeRef recordFolder) + { + if (recordsManagementService.isRecord(recordFolder) == true) + { + processRecord(recordFolder); + } + else if (recordsManagementService.isRecordFolder(recordFolder) == true) + { + for (NodeRef record : recordsManagementService.getRecords(recordFolder)) + { + processRecord(record); + } + } + } + + private void processRecord(NodeRef record) + { + List fromAssocs = recordsManagementAdminService.getCustomReferencesFrom(record); + for (AssociationRef fromAssoc : fromAssocs) + { + if (reference.equals(fromAssoc.getTypeQName()) == true) + { + NodeRef nodeRef = fromAssoc.getTargetRef(); + doEventComplete(nodeRef); + } + } + + List toAssocs = recordsManagementAdminService.getCustomReferencesTo(record); + for (AssociationRef toAssoc : toAssocs) + { + if (reference.equals(toAssoc.getTypeQName()) == true) + { + NodeRef nodeRef = toAssoc.getSourceRef(); + doEventComplete(nodeRef); + } + } + } + + private void doEventComplete(NodeRef nodeRef) + { + DispositionAction da = dispositionService.getNextDispositionAction(nodeRef); + if (da != null) + { + List events = da.getEventCompletionDetails(); + for (EventCompletionDetails event : events) + { + RecordsManagementEvent rmEvent = recordsManagementEventService.getEvent(event.getEventName()); + if (event.isEventComplete() == false && + rmEvent.getType().equals(getName()) == true) + { + // Complete the event + Map params = new HashMap(3); + params.put(CompleteEventAction.PARAM_EVENT_NAME, event.getEventName()); + params.put(CompleteEventAction.PARAM_EVENT_COMPLETED_BY, AuthenticationUtil.getFullyAuthenticatedUser()); + params.put(CompleteEventAction.PARAM_EVENT_COMPLETED_AT, new Date()); + recordsManagementActionService.executeRecordsManagementAction(nodeRef, "completeEvent", params); + + break; + } + } + } + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/event/RecordsManagementEvent.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/event/RecordsManagementEvent.java new file mode 100644 index 0000000000..da6e95e3a8 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/event/RecordsManagementEvent.java @@ -0,0 +1,79 @@ +/* + * 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.module.org_alfresco_module_rm.event; + +/** + * Records management event + * + * @author Roy Wetherall + */ +public class RecordsManagementEvent +{ + /** Records management event type */ + private String type; + + /** Records management event name */ + private String name; + + /** Records management display label */ + private String displayLabel; + + /** + * Constructor + * + * @param type event type + * @param name event name + * @param displayLabel event display label + */ + public RecordsManagementEvent(String type, String name, String displayLabel) + { + this.type = type; + this.name = name; + this.displayLabel = displayLabel; + } + + /** + * Get records management type + * + * @return String records management type + */ + public String getType() + { + return this.type; + } + + /** + * Event name + * + * @return String event name + */ + public String getName() + { + return this.name; + } + + /** + * + * @return + */ + public String getDisplayLabel() + { + return displayLabel; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/event/RecordsManagementEventService.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/event/RecordsManagementEventService.java new file mode 100644 index 0000000000..5e60e5b64e --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/event/RecordsManagementEventService.java @@ -0,0 +1,92 @@ +/* + * 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.module.org_alfresco_module_rm.event; + +import java.util.List; + +/** + * Records management event service interface + * + * @author Roy Wetherall + */ +public interface RecordsManagementEventService +{ + /** + * Register an event type + * + * @param eventType event type + */ + void registerEventType(RecordsManagementEventType eventType); + + /** + * Get a list of the event types + * + * @return List list of the event types + */ + List getEventTypes(); + + /** + * Get the records management event type + * + * @param eventType name + * @return RecordsManagementEventType event type + */ + RecordsManagementEventType getEventType(String eventTypeName); + + /** + * Get the list of available events + * + * @return List list of events + */ + List getEvents(); + + /** + * Get a records management event given its name. Returns null if the event name is not + * recognised. + * + * @param eventName event name + * @return RecordsManagementEvent event + */ + RecordsManagementEvent getEvent(String eventName); + + /** + * Indicates whether a perticular event exists. Returns true if it does, false otherwise. + * + * @param eventName event name + * @return boolean true if event exists, false otherwise + */ + boolean existsEvent(String eventName); + + /** + * Add an event + * + * @param eventType event type + * @param eventName event name + * @param eventDisplayLabel event display label + */ + RecordsManagementEvent addEvent(String eventType, String eventName, String eventDisplayLabel); + + /** + * Remove an event + * + * @param eventName event name + */ + void removeEvent(String eventName); + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/event/RecordsManagementEventServiceImpl.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/event/RecordsManagementEventServiceImpl.java new file mode 100644 index 0000000000..b5084747d7 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/event/RecordsManagementEventServiceImpl.java @@ -0,0 +1,280 @@ +/* + * 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.module.org_alfresco_module_rm.event; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +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; +import org.alfresco.service.cmr.repository.StoreRef; +import org.json.JSONArray; +import org.json.JSONObject; + +/** + * Records management event service implementation + * + * @author Roy Wetherall + */ +public class RecordsManagementEventServiceImpl implements RecordsManagementEventService +{ + /** Reference to the rm event config node */ + private static final StoreRef SPACES_STORE = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "SpacesStore"); + private static final NodeRef CONFIG_NODE_REF = new NodeRef(SPACES_STORE, "rm_event_config"); + + /** Node service */ + private NodeService nodeService; + + /** Content service */ + private ContentService contentService; + + /** Registered event types */ + private Map eventTypes = new HashMap(7); + + /** Available events */ + private Map events; + + /** + * Set the node service + * + * @param nodeService node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Set the content service + * + * @param contentService content service + */ + public void setContentService(ContentService contentService) + { + this.contentService = contentService; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEventService#registerEventType(org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEventType) + */ + public void registerEventType(RecordsManagementEventType eventType) + { + this.eventTypes.put(eventType.getName(), eventType); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEventService#getEventTypes() + */ + public List getEventTypes() + { + return new ArrayList(this.eventTypes.values()); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEventService#getEvents() + */ + public List getEvents() + { + return new ArrayList(this.getEventMap().values()); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEventService#getEvent(java.lang.String) + */ + public RecordsManagementEvent getEvent(String eventName) + { + if (getEventMap().containsKey(eventName) == false) + { + throw new AlfrescoRuntimeException("The event " + eventName + " does not exist."); + } + return getEventMap().get(eventName); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEventService#existsEvent(java.lang.String) + */ + public boolean existsEvent(String eventName) + { + return getEventMap().containsKey(eventName); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEventService#addEvent(java.lang.String, java.lang.String, java.lang.String) + */ + public RecordsManagementEvent addEvent(String eventType, String eventName, String eventDisplayLabel) + { + // Check that the eventType is valid + if (eventTypes.containsKey(eventType) == false) + { + throw new AlfrescoRuntimeException( + "Can not add event because event " + + eventName + + " has an undefined eventType. (" + + eventType + ")"); + } + + // Create event and add to map + RecordsManagementEvent event = new RecordsManagementEvent(eventType, eventName, eventDisplayLabel); + getEventMap().put(event.getName(), event); + + // Persist the changes to the event list + saveEvents(); + + return new RecordsManagementEvent(eventType, eventName, eventDisplayLabel); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEventService#removeEvent(java.lang.String) + */ + public void removeEvent(String eventName) + { + // Remove the event from the map + getEventMap().remove(eventName); + + // Persist the changes to the event list + saveEvents(); + } + + /** + * Helper method to get the event map. Loads initial instance from persisted configuration file. + * + * @return Map map of available events by event name + */ + private Map getEventMap() + { + if (this.events == null) + { + loadEvents(); + } + return this.events; + } + + /** + * Load the events from the persistant storage + */ + private void loadEvents() + { + AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public Object doWork() throws Exception + { + // Get the event config node + if (nodeService.exists(CONFIG_NODE_REF) == false) + { + throw new AlfrescoRuntimeException("Unable to find records management event configuration node."); + } + + // Read content from config node + ContentReader reader = contentService.getReader(CONFIG_NODE_REF, ContentModel.PROP_CONTENT); + String jsonString = reader.getContentString(); + + JSONObject configJSON = new JSONObject(jsonString); + JSONArray eventsJSON = configJSON.getJSONArray("events"); + + events = new HashMap(eventsJSON.length()); + + for (int i = 0; i < eventsJSON.length(); i++) + { + // Get the JSON object that represents the event + JSONObject eventJSON = eventsJSON.getJSONObject(i); + + // Get the details of the event + String eventType = eventJSON.getString("eventType"); + String eventName = eventJSON.getString("eventName"); + String eventDisplayLabel = eventJSON.getString("eventDisplayLabel"); + + // Check that the eventType is valid + if (eventTypes.containsKey(eventType) == false) + { + throw new AlfrescoRuntimeException( + "Can not load rm event configuration because event " + + eventName + + " has an undefined eventType. (" + + eventType + ")"); + } + + // Create event and add to map + RecordsManagementEvent event = new RecordsManagementEvent(eventType, eventName, eventDisplayLabel); + events.put(event.getName(), event); + } + return null; + } + + }, AuthenticationUtil.getSystemUserName()); + } + + /** + * Save the events to the peristant storage + */ + private void saveEvents() + { + AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public Object doWork() throws Exception + { + // Get the event config node + if (nodeService.exists(CONFIG_NODE_REF) == false) + { + throw new AlfrescoRuntimeException("Unable to find records management event configuration node."); + } + + JSONObject configJSON = new JSONObject(); + JSONArray eventsJSON = new JSONArray(); + + int index = 0; + for (RecordsManagementEvent event : events.values()) + { + JSONObject eventJSON = new JSONObject(); + eventJSON.put("eventType", event.getType()); + eventJSON.put("eventName", event.getName()); + eventJSON.put("eventDisplayLabel", event.getDisplayLabel()); + + eventsJSON.put(index, eventJSON); + index++; + } + configJSON.put("events", eventsJSON); + + // Get content writer + ContentWriter contentWriter = contentService.getWriter(CONFIG_NODE_REF, ContentModel.PROP_CONTENT, true); + contentWriter.putContent(configJSON.toString()); + + return null; + } + + }, AuthenticationUtil.getSystemUserName()); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEventService#getEventType(java.lang.String) + */ + public RecordsManagementEventType getEventType(String eventTypeName) + { + return this.eventTypes.get(eventTypeName); + } +} + + diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/event/RecordsManagementEventType.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/event/RecordsManagementEventType.java new file mode 100644 index 0000000000..a87f9c8e83 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/event/RecordsManagementEventType.java @@ -0,0 +1,48 @@ +/* + * 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.module.org_alfresco_module_rm.event; + +/** + * Records management event type interface + * + * @author Roy Wetherall + */ +public interface RecordsManagementEventType +{ + /** + * Get the name of the records management event type + * + * @return String event type name + */ + String getName(); + + /** + * Gets the display label of the event type + * + * @return String display label + */ + String getDisplayLabel(); + + /** + * Indicates whether the event is automatic or not + * + * @return boolean true if automatic, false otherwise + */ + boolean isAutomaticEvent(); +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/event/SimpleRecordsManagementEventTypeImpl.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/event/SimpleRecordsManagementEventTypeImpl.java new file mode 100644 index 0000000000..1eef409477 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/event/SimpleRecordsManagementEventTypeImpl.java @@ -0,0 +1,98 @@ +/* + * 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.module.org_alfresco_module_rm.event; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.surf.util.I18NUtil; +import org.springframework.beans.factory.BeanNameAware; + +/** + * Simple records management event type implementation + * + * @author Roy Wetherall + */ +public class SimpleRecordsManagementEventTypeImpl implements RecordsManagementEventType, BeanNameAware +{ + /** Logger */ + @SuppressWarnings("unused") + private static Log logger = LogFactory.getLog(SimpleRecordsManagementEventTypeImpl.class); + + /** Display label lookup prefix */ + protected static final String LOOKUP_PREFIX = "rmeventservice."; + + /** Name */ + public static final String NAME = "rmEventType.simple"; + + /** Records management event service */ + protected RecordsManagementEventService recordsManagementEventService; + + /** Name */ + protected String name; + + /** + * Set the records management event service + * + * @param recordsManagementEventService records management service + */ + public void setRecordsManagementEventService(RecordsManagementEventService recordsManagementEventService) + { + this.recordsManagementEventService = recordsManagementEventService; + } + + /** + * Initialisation method + */ + public void init() + { + recordsManagementEventService.registerEventType(this); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEventType#isAutomaticEvent() + */ + public boolean isAutomaticEvent() + { + return false; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEventType#getName() + */ + public String getName() + { + return this.name; + } + + /** + * @see org.springframework.beans.factory.BeanNameAware#setBeanName(java.lang.String) + */ + public void setBeanName(String name) + { + this.name = name; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEventType#getDisplayLabel() + */ + public String getDisplayLabel() + { + return I18NUtil.getMessage(LOOKUP_PREFIX + getName()); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/forms/RecordsManagementFormFilter.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/forms/RecordsManagementFormFilter.java new file mode 100644 index 0000000000..9dddf5557d --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/forms/RecordsManagementFormFilter.java @@ -0,0 +1,142 @@ +/* + * 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.module.org_alfresco_module_rm.forms; + +import java.util.List; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminService; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementServiceRegistry; +import org.alfresco.module.org_alfresco_module_rm.dod5015.DOD5015Model; +import org.alfresco.repo.forms.FieldGroup; +import org.alfresco.repo.forms.Form; +import org.alfresco.repo.forms.FormData; +import org.alfresco.repo.forms.processor.AbstractFilter; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.NamespaceService; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Abstract base class for records management related form filter + * implementations. + * + * @author Gavin Cornwell + */ +public abstract class RecordsManagementFormFilter extends AbstractFilter +{ + /** Logger */ + @SuppressWarnings("unused") + private static Log logger = LogFactory.getLog(RecordsManagementFormFilter.class); + + public static final String CUSTOM_RM_FIELD_GROUP_ID = "rm-custom"; + + protected static final FieldGroup CUSTOM_RM_FIELD_GROUP = new FieldGroup(CUSTOM_RM_FIELD_GROUP_ID, null, false, + false, null); + + protected NamespaceService namespaceService; + protected NodeService nodeService; + protected RecordsManagementServiceRegistry rmServiceRegistry; + protected RecordsManagementService rmService; + protected RecordsManagementAdminService rmAdminService; + + /** + * Sets the NamespaceService instance + * + * @param namespaceService The NamespaceService instance + */ + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + /** + * Sets the node service + * + * @param nodeService The NodeService instance + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Sets the RecordsManagementServiceRegistry instance + * + * @param rmServiceRegistry The RecordsManagementServiceRegistry instance + */ + public void setRecordsManagementServiceRegistry(RecordsManagementServiceRegistry rmServiceRegistry) + { + this.rmServiceRegistry = rmServiceRegistry; + } + + /** + * Sets the RecordsManagementService instance + * + * @param rmService The RecordsManagementService instance + */ + public void setRecordsManagementService(RecordsManagementService rmService) + { + this.rmService = rmService; + } + + /** + * Sets the RecordsManagementAdminService instance + * + * @param rmAdminService The RecordsManagementAdminService instance + */ + public void setRecordsManagementAdminService(RecordsManagementAdminService rmAdminService) + { + this.rmAdminService = rmAdminService; + } + + /** + * @see + * org.alfresco.repo.forms.processor.Filter#beforePersist(java.lang.Object, + * org.alfresco.repo.forms.FormData) + */ + public void beforePersist(ItemType item, FormData data) + { + // ignored + } + + /** + * @see + * org.alfresco.repo.forms.processor.Filter#beforeGenerate(java.lang.Object, + * java.util.List, java.util.List, org.alfresco.repo.forms.Form, + * java.util.Map) + */ + public void beforeGenerate(ItemType item, List fields, List forcedFields, Form form, + Map context) + { + // ignored + } + + /** + * @see + * org.alfresco.repo.forms.processor.Filter#afterPersist(java.lang.Object, + * org.alfresco.repo.forms.FormData, java.lang.Object) + */ + public void afterPersist(ItemType item, FormData data, NodeRef persistedObject) + { + // ignored + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/forms/RecordsManagementNodeFormFilter.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/forms/RecordsManagementNodeFormFilter.java new file mode 100644 index 0000000000..b0a73bffda --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/forms/RecordsManagementNodeFormFilter.java @@ -0,0 +1,438 @@ +/* + * 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.module.org_alfresco_module_rm.forms; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.model.ImapModel; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionScheduleImpl; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService; +import org.alfresco.module.org_alfresco_module_rm.dod5015.DOD5015Model; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementCustomModel; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.repo.forms.Field; +import org.alfresco.repo.forms.FieldDefinition; +import org.alfresco.repo.forms.Form; +import org.alfresco.repo.forms.PropertyFieldDefinition; +import org.alfresco.repo.forms.processor.node.FieldUtils; +import org.alfresco.repo.forms.processor.node.FormFieldConstants; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Implementation of a form processor Filter. + *

+ * The filter ensures that any custom properties defined for the records + * management type are provided as part of the Form and also assigned to the + * same field group. + *

+ * + * @author Gavin Cornwell + */ +public class RecordsManagementNodeFormFilter extends RecordsManagementFormFilter implements RecordsManagementModel, DOD5015Model +{ + /** Logger */ + private static Log logger = LogFactory.getLog(RecordsManagementNodeFormFilter.class); + + protected static final String TRANSIENT_DECLARED = "rmDeclared"; + protected static final String TRANSIENT_RECORD_TYPE = "rmRecordType"; + protected static final String TRANSIENT_CATEGORY_ID = "rmCategoryIdentifier"; + protected static final String TRANSIENT_DISPOSITION_INSTRUCTIONS = "rmDispositionInstructions"; + + /** Dictionary service */ + protected DictionaryService dictionaryService; + + /** Disposition service */ + protected DispositionService dispositionService; + + /** + * Sets the data dictionary service + * + * @param dictionaryService The DictionaryService instance + */ + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + /** + * Sets the disposition service + * + * @param dispositionService disposition service + */ + public void setDispositionService(DispositionService dispositionService) + { + this.dispositionService = dispositionService; + } + + /* + * @see org.alfresco.repo.forms.processor.Filter#afterGenerate(java.lang.Object, java.util.List, java.util.List, org.alfresco.repo.forms.Form, java.util.Map) + */ + public void afterGenerate(NodeRef nodeRef, List fields, List forcedFields, Form form, + Map context) + { + // TODO this needs a massive refactor inorder to support any custom type or aspect .... + + // if the node has the RM marker aspect look for the custom properties + // for the type + if (this.nodeService.hasAspect(nodeRef, ASPECT_FILE_PLAN_COMPONENT)) + { + // Make sure any customisable types or aspects present of the node have their properties included + // in the custom group + showCustomProperties(nodeRef, form); + + if (this.nodeService.hasAspect(nodeRef, ASPECT_RECORD)) + { + // force the "supplementalMarkingList" property to be present + forceSupplementalMarkingListProperty(form, nodeRef); + + // generate property definitions for the 'transient' properties + generateDeclaredPropertyField(form, nodeRef); + generateRecordTypePropertyField(form, nodeRef); + generateCategoryIdentifierPropertyField(form, nodeRef); + generateDispositionInstructionsPropertyField(form, nodeRef); + + // if the record is the result of an email we need to 'protect' some fields + if (this.nodeService.hasAspect(nodeRef, ImapModel.ASPECT_IMAP_CONTENT)) + { + protectEmailExtractedFields(form, nodeRef); + } + } + else + { + QName type = this.nodeService.getType(nodeRef); + if (TYPE_RECORD_FOLDER.equals(type)) + { + // force the "supplementalMarkingList" property to be present + forceSupplementalMarkingListProperty(form, nodeRef); + } + else if (TYPE_DISPOSITION_SCHEDULE.equals(type)) + { + // use the same mechanism used to determine whether steps can be removed from the + // schedule to determine whether the disposition level can be changed i.e. record + // level or folder level. + DispositionSchedule schedule = new DispositionScheduleImpl(this.rmServiceRegistry, this.nodeService, nodeRef); + if (dispositionService.hasDisposableItems(schedule) == true) + { + protectRecordLevelDispositionPropertyField(form); + } + } + } + } + } + + /** + * Show the custom properties if any are present. + * + * @param nodeRef node reference + * @param form form + */ + protected void showCustomProperties(NodeRef nodeRef, Form form) + { + Set customClasses = rmAdminService.getCustomisable(nodeRef); + if (customClasses.isEmpty() == false) + { + // add the 'rm-custom' field group + addCustomRMGroup(form); + } + } + + /** + * Adds the Custom RM field group (id 'rm-custom') to all the field + * definitions representing RM custom properties. + * + * @param form The form holding the field definitions + */ + protected void addCustomRMGroup(Form form) + { + // iterate round existing fields and set group on each custom + // RM field + List fieldDefs = form.getFieldDefinitions(); + for (FieldDefinition fieldDef : fieldDefs) + { + if (fieldDef.getName().startsWith(RM_CUSTOM_PREFIX) && + !fieldDef.getName().equals(PROP_SUPPLEMENTAL_MARKING_LIST.toPrefixString(this.namespaceService))) + { + // only add custom RM properties, not associations/references + if (fieldDef instanceof PropertyFieldDefinition) + { + fieldDef.setGroup(CUSTOM_RM_FIELD_GROUP); + + if (logger.isDebugEnabled()) + logger.debug("Added \"" + fieldDef.getName() + "\" to RM custom field group"); + } + } + } + } + + /** + * Forces the "rmc:supplementalMarkingList" property to be present, if it is + * already on the given node this method does nothing, otherwise a property + * field definition is generated for the property. + * + * @param form The Form instance to add the property to + * @param nodeRef The node the form is being generated for + */ + protected void forceSupplementalMarkingListProperty(Form form, NodeRef nodeRef) + { + if (!this.nodeService.hasAspect(nodeRef, + RecordsManagementCustomModel.ASPECT_SUPPLEMENTAL_MARKING_LIST)) + { + PropertyDefinition propDef = this.dictionaryService.getProperty( + RecordsManagementCustomModel.PROP_SUPPLEMENTAL_MARKING_LIST); + + if (propDef != null) + { + Field field = FieldUtils.makePropertyField(propDef, null, null, namespaceService); + form.addField(field); + } + else if (logger.isWarnEnabled()) + { + logger.warn("Could not add " + + RecordsManagementCustomModel.PROP_SUPPLEMENTAL_MARKING_LIST.getLocalName() + + " property as it's definition could not be found"); + } + } + } + + /** + * Generates the field definition for the transient rmDeclared + * property. + * + * @param form The Form instance to add the property to + * @param nodeRef The node the form is being generated for + */ + protected void generateDeclaredPropertyField(Form form, NodeRef nodeRef) + { + // TODO should this be done using a new FieldProcessor? + String dataKeyName = FormFieldConstants.PROP_DATA_PREFIX + TRANSIENT_DECLARED; + PropertyFieldDefinition declaredField = new PropertyFieldDefinition(TRANSIENT_DECLARED, + DataTypeDefinition.BOOLEAN.getLocalName()); + declaredField.setLabel(TRANSIENT_DECLARED); + declaredField.setDescription(TRANSIENT_DECLARED); + declaredField.setProtectedField(true); + declaredField.setDataKeyName(dataKeyName); + form.addFieldDefinition(declaredField); + form.addData(dataKeyName, this.nodeService.hasAspect(nodeRef, ASPECT_DECLARED_RECORD)); + } + + /** + * Generates the field definition for the transient + * rmRecordType property + * + * @param form The Form instance to add the property to + * @param nodeRef The node the form is being generated for + */ + protected void generateRecordTypePropertyField(Form form, NodeRef nodeRef) + { + String dataKeyName = FormFieldConstants.PROP_DATA_PREFIX + TRANSIENT_RECORD_TYPE; + PropertyFieldDefinition recordTypeField = new PropertyFieldDefinition(TRANSIENT_RECORD_TYPE, + DataTypeDefinition.TEXT.getLocalName()); + recordTypeField.setLabel(TRANSIENT_RECORD_TYPE); + recordTypeField.setDescription(TRANSIENT_RECORD_TYPE); + recordTypeField.setProtectedField(true); + recordTypeField.setDataKeyName(dataKeyName); + form.addFieldDefinition(recordTypeField); + + // determine what record type value to return, use aspect/type title + // from model + String recordType = null; + QName type = this.nodeService.getType(nodeRef); + if (TYPE_NON_ELECTRONIC_DOCUMENT.equals(type)) + { + // get the non-electronic type title + recordType = dictionaryService.getType(TYPE_NON_ELECTRONIC_DOCUMENT).getTitle(); + } + else + { + // the aspect applied to record determines it's type + if (nodeService.hasAspect(nodeRef, ASPECT_PDF_RECORD)) + { + recordType = dictionaryService.getAspect(ASPECT_PDF_RECORD).getTitle(); + } + else if (nodeService.hasAspect(nodeRef, ASPECT_WEB_RECORD)) + { + recordType = dictionaryService.getAspect(ASPECT_WEB_RECORD).getTitle(); + } + else if (nodeService.hasAspect(nodeRef, ASPECT_SCANNED_RECORD)) + { + recordType = dictionaryService.getAspect(ASPECT_SCANNED_RECORD).getTitle(); + } + else if (nodeService.hasAspect(nodeRef, ASPECT_DIGITAL_PHOTOGRAPH_RECORD)) + { + recordType = dictionaryService.getAspect(ASPECT_DIGITAL_PHOTOGRAPH_RECORD).getTitle(); + } + else + { + // no specific aspect applied so default to just "Record" + recordType = dictionaryService.getAspect(ASPECT_RECORD).getTitle(); + } + } + + form.addData(dataKeyName, recordType); + } + + /** + * Generates the field definition for the transient rmCategoryIdentifier + * property + * + * @param form The Form instance to add the property to + * @param nodeRef The node the form is being generated for + */ + protected void generateDispositionInstructionsPropertyField(Form form, NodeRef nodeRef) + { + String dataKeyName = FormFieldConstants.PROP_DATA_PREFIX + TRANSIENT_DISPOSITION_INSTRUCTIONS; + PropertyFieldDefinition dispInstructionsField = new PropertyFieldDefinition(TRANSIENT_DISPOSITION_INSTRUCTIONS, + DataTypeDefinition.TEXT.getLocalName()); + dispInstructionsField.setLabel(TRANSIENT_DISPOSITION_INSTRUCTIONS); + dispInstructionsField.setDescription(TRANSIENT_DISPOSITION_INSTRUCTIONS); + dispInstructionsField.setProtectedField(true); + dispInstructionsField.setDataKeyName(dataKeyName); + form.addFieldDefinition(dispInstructionsField); + + // use RMService to get disposition instructions + DispositionSchedule ds = dispositionService.getDispositionSchedule(nodeRef); + if (ds != null) + { + String instructions = ds.getDispositionInstructions(); + if (instructions != null) + { + form.addData(dataKeyName, instructions); + } + } + } + + /** + * Generates the field definition for the transient rmCategoryIdentifier + * property + * + * @param form The Form instance to add the property to + * @param nodeRef The node the form is being generated for + */ + protected void generateCategoryIdentifierPropertyField(Form form, NodeRef nodeRef) + { + String dataKeyName = FormFieldConstants.PROP_DATA_PREFIX + TRANSIENT_CATEGORY_ID; + PropertyFieldDefinition categoryIdField = new PropertyFieldDefinition(TRANSIENT_CATEGORY_ID, + DataTypeDefinition.TEXT.getLocalName()); + categoryIdField.setLabel(TRANSIENT_CATEGORY_ID); + categoryIdField.setDescription(TRANSIENT_CATEGORY_ID); + categoryIdField.setProtectedField(true); + categoryIdField.setDataKeyName(dataKeyName); + form.addFieldDefinition(categoryIdField); + + // get the category id from the appropriate parent node + NodeRef category = getRecordCategory(nodeRef); + if (category != null) + { + String categoryId = (String)nodeService.getProperty(category, PROP_IDENTIFIER); + if (categoryId != null) + { + form.addData(dataKeyName, categoryId); + } + } + } + + /** + * Marks all the fields that contain data extracted from an email + * as protected fields. + * + * @param form The Form instance to add the property to + * @param nodeRef The node the form is being generated for + */ + protected void protectEmailExtractedFields(Form form, NodeRef nodeRef) + { + // iterate round existing fields and set email fields as protected + List fieldDefs = form.getFieldDefinitions(); + for (FieldDefinition fieldDef : fieldDefs) + { + if (fieldDef.getName().equals("cm:title") || + fieldDef.getName().equals("cm:author") || + fieldDef.getName().equals("rma:originator") || + fieldDef.getName().equals("rma:publicationDate") || + fieldDef.getName().equals("rma:dateReceived") || + fieldDef.getName().equals("rma:address") || + fieldDef.getName().equals("rma:otherAddress")) + { + fieldDef.setProtectedField(true); + } + } + + if (logger.isDebugEnabled()) + logger.debug("Set email related fields to be protected"); + } + + /** + * Marks the recordLevelDisposition property as protected to disable editing + * + * @param form The Form instance + */ + protected void protectRecordLevelDispositionPropertyField(Form form) + { + // iterate round existing fields and set email fields as protected + List fieldDefs = form.getFieldDefinitions(); + for (FieldDefinition fieldDef : fieldDefs) + { + if (fieldDef.getName().equals(RecordsManagementModel.PROP_RECORD_LEVEL_DISPOSITION.toPrefixString( + this.namespaceService))) + { + fieldDef.setProtectedField(true); + break; + } + } + + if (logger.isDebugEnabled()) + logger.debug("Set 'rma:recordLevelDisposition' field to be protected as record folders or records are present"); + } + + /** + * Retrieves the record category the given record belongs to or + * null if the record category can not be found + * + * @param record NodeRef representing the record + * @return NodeRef of the record's category + */ + protected NodeRef getRecordCategory(NodeRef record) + { + NodeRef result = null; + + NodeRef parent = this.nodeService.getPrimaryParent(record).getParentRef(); + if (parent != null) + { + QName nodeType = this.nodeService.getType(parent); + if (this.dictionaryService.isSubClass(nodeType, TYPE_RECORD_CATEGORY)) + { + result = parent; + } + else + { + result = getRecordCategory(parent); + } + } + + return result; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/forms/RecordsManagementTypeFormFilter.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/forms/RecordsManagementTypeFormFilter.java new file mode 100644 index 0000000000..cf298cfa3f --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/forms/RecordsManagementTypeFormFilter.java @@ -0,0 +1,219 @@ +/* + * 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.module.org_alfresco_module_rm.forms; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.identifier.IdentifierService; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.repo.forms.Field; +import org.alfresco.repo.forms.FieldDefinition; +import org.alfresco.repo.forms.FieldGroup; +import org.alfresco.repo.forms.Form; +import org.alfresco.repo.forms.FormData; +import org.alfresco.repo.forms.processor.node.FieldUtils; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.dictionary.TypeDefinition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.surf.util.ParameterCheck; + +/** + * Implementation of a form processor Filter. + *

+ * The filter implements the afterGenerate method to ensure a + * default unique identifier is provided for the rma:identifier + * property. + *

+ *

+ * The filter also ensures that any custom properties defined for the records + * management type are provided as part of the Form. + *

+ * + * @author Gavin Cornwell + */ +public class RecordsManagementTypeFormFilter extends RecordsManagementFormFilter implements RecordsManagementModel +{ + /** Logger */ + private static Log logger = LogFactory.getLog(RecordsManagementTypeFormFilter.class); + + protected static final String NAME_FIELD_GROUP_ID = "name"; + protected static final String TITLE_FIELD_GROUP_ID = "title"; + protected static final String DESC_FIELD_GROUP_ID = "description"; + protected static final String OTHER_FIELD_GROUP_ID = "other"; + + protected static final FieldGroup NAME_FIELD_GROUP = new FieldGroup(NAME_FIELD_GROUP_ID, null, false, false, null); + protected static final FieldGroup TITLE_FIELD_GROUP = new FieldGroup(TITLE_FIELD_GROUP_ID, null, false, false, null); + protected static final FieldGroup DESC_FIELD_GROUP = new FieldGroup(DESC_FIELD_GROUP_ID, null, false, false, null); + protected static final FieldGroup OTHER_FIELD_GROUP = new FieldGroup(OTHER_FIELD_GROUP_ID, null, false, false, null); + + /** Identifier service */ + protected IdentifierService identifierService; + + /** + * @param identifierService identifier service + */ + public void setIdentifierService(IdentifierService identifierService) + { + this.identifierService = identifierService; + } + + /* + * @see + * org.alfresco.repo.forms.processor.Filter#afterGenerate(java.lang.Object, + * java.util.List, java.util.List, org.alfresco.repo.forms.Form, + * java.util.Map) + */ + public void afterGenerate(TypeDefinition type, List fields, List forcedFields, Form form, + Map context) + { + QName typeName = type.getName(); + if (rmAdminService.isCustomisable(typeName) == true) + { + addCustomRMProperties(typeName, form); + } + + // What about any mandatory aspects? + Set aspects = type.getDefaultAspectNames(); + for (QName aspect : aspects) + { + if (rmAdminService.isCustomisable(aspect) == true) + { + addCustomRMProperties(aspect, form); + } + } + + // Group fields + groupFields(form); + } + + /** + * Adds a property definition for each of the custom properties for the + * given RM type to the given form. + * + * @param rmTypeCustomAspect Enum representing the RM type to add custom + * properties for + * @param form The form to add the properties to + */ + protected void addCustomRMProperties(QName customisableType, Form form) + { + ParameterCheck.mandatory("customisableType", customisableType); + ParameterCheck.mandatory("form", form); + + Map customProps = rmAdminService.getCustomPropertyDefinitions(customisableType); + + if (logger.isDebugEnabled() == true) + { + logger.debug("Found " + customProps.size() + " custom properties for customisable type " + customisableType); + } + + // setup field definition for each custom property + Collection properties = customProps.values(); + List fields = FieldUtils.makePropertyFields(properties, CUSTOM_RM_FIELD_GROUP, namespaceService); + form.addFields(fields); + } + + /* + * @see org.alfresco.repo.forms.processor.Filter#afterPersist(java.lang.Object, org.alfresco.repo.forms.FormData, java.lang.Object) + */ + public void afterPersist(TypeDefinition item, FormData data, final NodeRef nodeRef) + { + } + + /** + * Generates a unique identifier for the given node (based on the dbid). + * + * @param nodeRef The NodeRef to generate a unique id for + * @return The identifier + */ + protected String generateIdentifier(NodeRef nodeRef) + { + String identifier = identifierService.generateIdentifier(nodeRef); + if (logger.isDebugEnabled() == true) + { + logger.debug("Generated '" + identifier + "' for unique identifier"); + } + return identifier; + } + + /** + * Function to pad a string with zero '0' characters to the required length + * + * @param s String to pad with leading zero '0' characters + * @param len Length to pad to + * @return padded string or the original if already at >=len characters + */ + protected String padString(String s, int len) + { + String result = s; + + for (int i = 0; i < (len - s.length()); i++) + { + result = "0" + result; + } + + return result; + } + + /** + * Puts all fields in a group to workaround ALF-6089. + * + * @param form The form being generated + */ + protected void groupFields(Form form) + { + // to control the order of the fields add the name, title and description fields to + // a field group containing just that field, all other fields that are not already + // in a group go into an "other" field group. The client config can then declare a + // client side set with the same id and order them correctly. + + List fieldDefs = form.getFieldDefinitions(); + for (FieldDefinition fieldDef : fieldDefs) + { + FieldGroup group = fieldDef.getGroup(); + if (group == null) + { + if (fieldDef.getName().equals(ContentModel.PROP_NAME.toPrefixString(this.namespaceService))) + { + fieldDef.setGroup(NAME_FIELD_GROUP); + } + else if (fieldDef.getName().equals(ContentModel.PROP_TITLE.toPrefixString(this.namespaceService))) + { + fieldDef.setGroup(TITLE_FIELD_GROUP); + } + else if (fieldDef.getName().equals(ContentModel.PROP_DESCRIPTION.toPrefixString(this.namespaceService))) + { + fieldDef.setGroup(DESC_FIELD_GROUP); + } + else + { + fieldDef.setGroup(OTHER_FIELD_GROUP); + } + } + } + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/identifier/BasicIdentifierGenerator.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/identifier/BasicIdentifierGenerator.java new file mode 100644 index 0000000000..42165ad5b5 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/identifier/BasicIdentifierGenerator.java @@ -0,0 +1,57 @@ +/* + * 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.module.org_alfresco_module_rm.identifier; + +import java.io.Serializable; +import java.util.Calendar; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Basic identifier generator implementation. + * + * @author Roy Wetherall + */ +public class BasicIdentifierGenerator extends IdentifierGeneratorBase +{ + /** + * @see org.alfresco.module.org_alfresco_module_rm.identifier.IdentifierGenerator#generateId(java.util.Map) + */ + @Override + public String generateId(Map context) + { + NodeRef nodeRef = (NodeRef)context.get(IdentifierService.CONTEXT_NODEREF); + Long dbId = 0l; + if (nodeRef != null) + { + dbId = (Long)nodeService.getProperty(nodeRef, ContentModel.PROP_NODE_DBID); + } + else + { + dbId = System.currentTimeMillis(); + } + + Calendar fileCalendar = Calendar.getInstance(); + String year = Integer.toString(fileCalendar.get(Calendar.YEAR)); + String identifier = year + "-" + padString(dbId.toString(), 10); + return identifier; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/identifier/IdentifierGenerator.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/identifier/IdentifierGenerator.java new file mode 100644 index 0000000000..666c2b353c --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/identifier/IdentifierGenerator.java @@ -0,0 +1,45 @@ +/* + * 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.module.org_alfresco_module_rm.identifier; + +import java.io.Serializable; +import java.util.Map; + +import org.alfresco.service.namespace.QName; + +/** + * Generates an identifier for a content type from a given context. + * + * @author Roy Wetherall + */ +public interface IdentifierGenerator +{ + /** + * The content type this generator is applicible to. + * @return QName the type + */ + QName getType(); + + /** + * Generates the next id based on the provided context. + * @param context map of context values + * @return String the next id + */ + String generateId(Map context); +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/identifier/IdentifierGeneratorBase.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/identifier/IdentifierGeneratorBase.java new file mode 100644 index 0000000000..c875210571 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/identifier/IdentifierGeneratorBase.java @@ -0,0 +1,103 @@ +/* + * 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.module.org_alfresco_module_rm.identifier; + +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.QName; + +/** + * @author Roy Wetherall + */ +public abstract class IdentifierGeneratorBase implements IdentifierGenerator +{ + /** Identifier service */ + private IdentifierService identifierService; + + /** Node service */ + protected NodeService nodeService; + + /** Content type */ + private QName type; + + /** + * Initialisation method + */ + public void init() + { + identifierService.register(this); + } + + /** + * Set identifier service. + * + * @param identifierService identifier service + */ + public void setIdentifierService(IdentifierService identifierService) + { + this.identifierService = identifierService; + } + + /** + * Set the node service + * + * @param nodeService node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Set type. + * + * @param type content type + */ + public void setTypeAsString(String type) + { + this.type = QName.createQName(type); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.identifier.IdentifierGenerator#getType() + */ + @Override + public QName getType() + { + return type; + } + + /** + * Function to pad a string with zero '0' characters to the required length + * + * @param s String to pad with leading zero '0' characters + * @param len Length to pad to + * @return padded string or the original if already at >=len characters + */ + protected String padString(String s, int len) + { + String result = s; + + for (int i = 0; i < (len - s.length()); i++) + { + result = "0" + result; + } + + return result; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/identifier/IdentifierService.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/identifier/IdentifierService.java new file mode 100644 index 0000000000..de9ad851df --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/identifier/IdentifierService.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.module.org_alfresco_module_rm.identifier; + +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + +/** + * Records management identifier service + * + * @author Roy Wetherall + */ +public interface IdentifierService +{ + /** Context value names */ + public static final String CONTEXT_NODEREF = "noderef"; + public static final String CONTEXT_PARENT_NODEREF = "parentndoeref"; + public static final String CONTEXT_ORIG_TYPE = "origionaltype"; + + /** + * Register an identifier generator implementation with the service. + * + * @param identifierGenerator identifier generator implementation + */ + void register(IdentifierGenerator identifierGenerator); + + /** + * Generate an identifier for a node with the given type and parent. + * + * @param type type of the node + * @param parent parent of the ndoe + * @return String generated identifier + */ + String generateIdentifier(QName type, NodeRef parent); + + /** + * Generate an identifier for the given node. + * + * @param nodeRef node reference + * @return String generated identifier + */ + String generateIdentifier(NodeRef nodeRef); +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/identifier/IdentifierServiceImpl.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/identifier/IdentifierServiceImpl.java new file mode 100644 index 0000000000..e9d99efd5f --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/identifier/IdentifierServiceImpl.java @@ -0,0 +1,196 @@ +/* + * 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.module.org_alfresco_module_rm.identifier; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.service.cmr.dictionary.ClassDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +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.namespace.QName; +import org.alfresco.util.ParameterCheck; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * @author Roy Wetherall + */ +public class IdentifierServiceImpl implements IdentifierService +{ + /** Logger */ + private static Log logger = LogFactory.getLog(IdentifierServiceImpl.class); + + /** Registry map */ + private Map register = new HashMap(5); + + /** Node service */ + private NodeService nodeService; + + /** Dictionary service */ + private DictionaryService dictionaryService; + + /** + * Set the node service + * + * @param nodeService node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Set the dictionary service + * + * @param dictionaryService dictionary service + */ + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.identifier.IdentifierService#generateIdentifier(org.alfresco.service.namespace.QName, org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + public String generateIdentifier(QName type, NodeRef parent) + { + ParameterCheck.mandatory("type", type); + + // Build the context + Map context = new HashMap(2); + if (parent != null) + { + context.put(CONTEXT_PARENT_NODEREF, parent); + } + context.put(CONTEXT_ORIG_TYPE, type); + + // Generate the id + return generateIdentifier(type, context); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.identifier.IdentifierService#generateIdentifier(org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + public String generateIdentifier(NodeRef nodeRef) + { + ParameterCheck.mandatory("nodeRef", nodeRef); + + Map context = new HashMap(3); + + // Set the original type + QName type = nodeService.getType(nodeRef); + context.put(CONTEXT_ORIG_TYPE, type); + + // Set the parent reference + ChildAssociationRef assocRef = nodeService.getPrimaryParent(nodeRef); + if (assocRef != null && assocRef.getParentRef() != null) + { + context.put(CONTEXT_PARENT_NODEREF, assocRef.getParentRef()); + } + + // Set the node reference + context.put(CONTEXT_NODEREF, nodeRef); + + // Generate the identifier + return generateIdentifier(type, context); + + } + + /** + * Generate an identifier for a given type of object with the acompanying context. + * + * @param type content type + * @param context context + * @return String identifier + */ + private String generateIdentifier(QName type, Map context) + { + ParameterCheck.mandatory("type", type); + ParameterCheck.mandatory("context", context); + + // Get the identifier generator + IdentifierGenerator idGen = lookupGenerator(type); + if (idGen == null) + { + if (logger.isDebugEnabled() == true) + { + logger.debug("Unable to generate id for object of type " + type.toString() + ", because no identifier generator was found."); + } + throw new AlfrescoRuntimeException("Unable to generate id for object of type " + type.toString() + ", because no identifier generator was found."); + } + + // Generate the identifier + return idGen.generateId(context); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.identifier.IdentifierService#register(org.alfresco.module.org_alfresco_module_rm.identifier.IdentifierGenerator) + */ + public void register(IdentifierGenerator idGen) + { + register.put(idGen.getType(), idGen); + } + + /** + * + * @param type content type (could be aspect or type) + * @return + */ + private IdentifierGenerator lookupGenerator(QName type) + { + ParameterCheck.mandatory("type", type); + + if (logger.isDebugEnabled() == true) + { + logger.debug("Looking for idGenerator for type " + type.toString()); + } + + // Look for the generator related to the type + IdentifierGenerator result = register.get(type); + if (result == null) + { + // Check the parent type + ClassDefinition typeDef = dictionaryService.getClass(type); + if (typeDef != null) + { + QName parentType = typeDef.getParentName(); + if (parentType != null) + { + // Recurse to find parent type generator + result = lookupGenerator(parentType); + } + } + else + { + if (logger.isDebugEnabled() == true) + { + logger.debug("Unable to find type definition for " + type.toString() + " when generating identifier."); + } + } + } + return result; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/job/DispositionLifecycleJob.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/job/DispositionLifecycleJob.java new file mode 100644 index 0000000000..8f5b3d51ea --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/job/DispositionLifecycleJob.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2009-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.module.org_alfresco_module_rm.job; + +import java.util.List; + +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction; +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +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.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.transaction.TransactionService; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; + +/** + * The Disposition Lifecycle Job Finds all disposition action nodes which are + * for "retain" or "cutOff" actions Where asOf > now OR + * dispositionEventsEligible = true; + * + * Runs the cut off or retain action for + * elligible records. + * + * @author mrogers + */ +public class DispositionLifecycleJob implements Job +{ + private static Log logger = LogFactory.getLog(DispositionLifecycleJob.class); + + /** + * @see org.quartz.Job#execute(org.quartz.JobExecutionContext) + */ + public void execute(JobExecutionContext context) throws JobExecutionException + { + final RecordsManagementActionService rmActionService = (RecordsManagementActionService) context + .getJobDetail().getJobDataMap().get("recordsManagementActionService"); + final NodeService nodeService = (NodeService) context.getJobDetail().getJobDataMap().get( + "nodeService"); + final SearchService search = (SearchService) context.getJobDetail().getJobDataMap().get( + "searchService"); + final TransactionService trxService = (TransactionService) context.getJobDetail() + .getJobDataMap().get("transactionService"); + + logger.debug("Job Starting"); + + AuthenticationUtil.runAs(new RunAsWork() + { + public Object doWork() throws Exception + { + StringBuilder sb = new StringBuilder(); + sb.append("+TYPE:\"rma:dispositionAction\" "); + sb.append("+(@rma\\:dispositionAction:(\"cutoff\" OR \"retain\"))"); + sb.append("+ISNULL:\"rma:dispositionActionCompletedAt\" "); + sb.append("+( "); + sb.append("@rma\\:dispositionEventsEligible:true "); + sb.append("OR @rma\\:dispositionAsOf:[MIN TO NOW] "); + sb.append(") "); + + String query = sb.toString(); + + ResultSet results = search.query(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, + SearchService.LANGUAGE_LUCENE, query); + List resultNodes = results.getNodeRefs(); + results.close(); + + RetryingTransactionHelper trn = trxService.getRetryingTransactionHelper(); + + for (NodeRef node : resultNodes) + { + final NodeRef currentNode = node; + + RetryingTransactionCallback processTranCB = new RetryingTransactionCallback() + { + public Boolean execute() throws Throwable + { + final String dispAction = (String) nodeService.getProperty(currentNode, + RecordsManagementModel.PROP_DISPOSITION_ACTION); + + // Run "retain" and "cutoff" actions. + + if (dispAction != null) + { + if (dispAction.equalsIgnoreCase("cutoff") || + dispAction.equalsIgnoreCase("retain")) + { + ChildAssociationRef parent = nodeService.getPrimaryParent(currentNode); + if (parent.getTypeQName().equals(RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION)) + { + // Check that the action is executable + RecordsManagementAction rmAction = rmActionService.getDispositionAction(dispAction); + if (rmAction.isExecutable(parent.getParentRef(), null) == true) + { + rmActionService.executeRecordsManagementAction(parent.getParentRef(), dispAction); + if (logger.isDebugEnabled()) + { + logger.debug("Processed action: " + dispAction + "on" + parent); + } + } + else + { + logger.debug("The disposition action " + dispAction + " is not executable."); + } + } + return null; + } + } + return Boolean.TRUE; + } + }; + /** + * Now do the work, one action in each transaction + */ + trn.doInTransaction(processTranCB); + } + return null; + }; + + }, AuthenticationUtil.getSystemUserName()); + + logger.debug("Job Finished"); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/job/NotifyOfRecordsDueForReviewJob.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/job/NotifyOfRecordsDueForReviewJob.java new file mode 100644 index 0000000000..8b29d60e0b --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/job/NotifyOfRecordsDueForReviewJob.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2009-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.module.org_alfresco_module_rm.job; + +import java.util.List; + +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.module.org_alfresco_module_rm.notification.RecordsManagementNotificationHelper; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +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.ResultSet; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.transaction.TransactionService; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; + +/** + * This job finds all Vital Records which are due for review, optionally + * excluding those for which notification has already been issued. + * + * @author Neil McErlean + */ +public class NotifyOfRecordsDueForReviewJob implements Job +{ + private static Log logger = LogFactory.getLog(NotifyOfRecordsDueForReviewJob.class); + + /** + * @see org.quartz.Job#execute(org.quartz.JobExecutionContext) + */ + public void execute(JobExecutionContext context) throws JobExecutionException + { + final RecordsManagementNotificationHelper notificationHelper = (RecordsManagementNotificationHelper)context.getJobDetail().getJobDataMap().get("recordsManagementNotificationHelper"); + final NodeService nodeService = (NodeService) context.getJobDetail().getJobDataMap().get("nodeService"); + final SearchService searchService = (SearchService) context.getJobDetail().getJobDataMap().get("searchService"); + final TransactionService trxService = (TransactionService) context.getJobDetail().getJobDataMap().get("transactionService"); + + if (logger.isDebugEnabled()) + { + logger.debug("Job " + this.getClass().getSimpleName() + " starting."); + } + + AuthenticationUtil.runAs(new RunAsWork() + { + public Object doWork() throws Exception + { + // Query is for all records that are due for review and for which + // notification has not been sent. + StringBuilder queryBuffer = new StringBuilder(); + queryBuffer.append("+ASPECT:\"rma:vitalRecord\" "); + queryBuffer.append("+(@rma\\:reviewAsOf:[MIN TO NOW] ) "); + queryBuffer.append("+( "); + queryBuffer.append("@rma\\:notificationIssued:false "); + queryBuffer.append("OR ISNULL:\"rma:notificationIssued\" "); + queryBuffer.append(") "); + String query = queryBuffer.toString(); + + ResultSet results = searchService.query(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, SearchService.LANGUAGE_LUCENE, query); + final List resultNodes = results.getNodeRefs(); + results.close(); + + if (logger.isDebugEnabled() == true) + { + logger.debug("Found " + resultNodes.size() + " nodes due for review and without notification."); + } + + //If we have something to do and a template to do it with + if(resultNodes.size() != 0) + { + RetryingTransactionHelper trn = trxService.getRetryingTransactionHelper(); + + //Send the email message - but we must not retry since email is not transactional + RetryingTransactionCallback txCallbackSendEmail = new RetryingTransactionCallback() + { + // Set the notification issued property. + public Boolean execute() throws Throwable + { + // Send notification + notificationHelper.recordsDueForReviewEmailNotification(resultNodes); + + return null; + } + }; + + RetryingTransactionCallback txUpdateNodesCallback = new RetryingTransactionCallback() + { + // Set the notification issued property. + public Boolean execute() throws Throwable + { + for (NodeRef node : resultNodes) + { + nodeService.setProperty(node, RecordsManagementModel.PROP_NOTIFICATION_ISSUED, "true"); + } + return Boolean.TRUE; + } + }; + + /** + * Now do the work, one action in each transaction + */ + trn.setMaxRetries(0); // don't retry the send email + trn.doInTransaction(txCallbackSendEmail); + trn.setMaxRetries(10); + trn.doInTransaction(txUpdateNodesCallback); + } + return null; + } + + }, AuthenticationUtil.getSystemUserName()); + + if (logger.isDebugEnabled()) + { + logger.debug("Job " + this.getClass().getSimpleName() + " finished"); + } + } // end of execute method + +} + + diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/job/PublishUpdatesJob.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/job/PublishUpdatesJob.java new file mode 100644 index 0000000000..0d998729e2 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/job/PublishUpdatesJob.java @@ -0,0 +1,320 @@ +/* + * Copyright (C) 2009-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.module.org_alfresco_module_rm.job; + +import java.util.List; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.module.org_alfresco_module_rm.job.publish.PublishExecutor; +import org.alfresco.module.org_alfresco_module_rm.job.publish.PublishExecutorRegistry; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.repo.policy.BehaviourFilter; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +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.ResultSet; +import org.alfresco.service.cmr.search.SearchService; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.quartz.Job; +import org.quartz.JobDataMap; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; + +/** + * Job to publish any pending updates on marked node references. + * + * @author Roy Wetherall + */ +public class PublishUpdatesJob implements Job, RecordsManagementModel +{ + /** Logger */ + private static Log logger = LogFactory.getLog(PublishUpdatesJob.class); + + /** Node service */ + private NodeService nodeService; + + /** Search service */ + private SearchService searchService; + + /** Retrying transaction helper */ + private RetryingTransactionHelper retryingTransactionHelper; + + /** Publish executor register */ + private PublishExecutorRegistry register; + + /** Behaviour filter */ + private BehaviourFilter behaviourFilter; + + /** Indicates whether the job bean has been initialised or not */ + private boolean initialised = false; + + /** + * @see org.quartz.Job#execute(org.quartz.JobExecutionContext) + */ + public void execute(JobExecutionContext context) throws JobExecutionException + { + // Initialise the service references + initServices(context); + + if (logger.isDebugEnabled() == true) + { + logger.debug("Job Starting"); + } + + AuthenticationUtil.runAs(new RunAsWork() + { + public Object doWork() throws Exception + { + // Get a list of the nodes that have updates that need to be published + List nodeRefs = getUpdatedNodes(); + + // Deal with each updated disposition action in turn + for (NodeRef nodeRef : nodeRefs) + { + // Mark the update node as publishing in progress + markPublishInProgress(nodeRef); + try + { + // Publish updates + publishUpdates(nodeRef); + } + finally + { + // Ensure the update node has either completed the publish or is marked as no longer in progress + unmarkPublishInProgress(nodeRef); + } + } + return null; + }; + }, AuthenticationUtil.getSystemUserName()); + + if (logger.isDebugEnabled() == true) + { + logger.debug("Job Finished"); + } + } + + /** + * Get a list of the nodes with updates pending publish + * @return List list of node refences with updates pending publication + */ + private List getUpdatedNodes() + { + RetryingTransactionCallback> execution = + new RetryingTransactionHelper.RetryingTransactionCallback>() + { + @Override + public List execute() throws Throwable + { + // Build the query string + StringBuilder sb = new StringBuilder(); + sb.append("+ASPECT:\"rma:").append(ASPECT_UNPUBLISHED_UPDATE.getLocalName()).append("\" "); + sb.append("@rma\\:").append(PROP_PUBLISH_IN_PROGRESS.getLocalName()).append(":false "); + String query = sb.toString(); + + if (logger.isDebugEnabled() == true) + { + logger.debug("Executing query " + query); + } + + // Execute query to find updates awaiting publishing + ResultSet results = searchService.query(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, + SearchService.LANGUAGE_LUCENE, query); + List resultNodes = results.getNodeRefs(); + results.close(); + + if (logger.isDebugEnabled() == true) + { + logger.debug("Found " + resultNodes.size() + " disposition action definitions updates awaiting publishing."); + } + + return resultNodes; + } + }; + return retryingTransactionHelper.doInTransaction(execution, true); + } + + /** + * Initialise service based on the job execution context + * @param context job execution context + */ + private void initServices(JobExecutionContext context) + { + if (initialised == false) + { + // Get references to the required services + JobDataMap jobDataMap = context.getJobDetail().getJobDataMap(); + nodeService = (NodeService)jobDataMap.get("nodeService"); + searchService = (SearchService)jobDataMap.get("searchService"); + retryingTransactionHelper = (RetryingTransactionHelper)jobDataMap.get("retryingTransactionHelper"); + register = (PublishExecutorRegistry)jobDataMap.get("publishExecutorRegistry"); + behaviourFilter = (BehaviourFilter)jobDataMap.get("behaviourFilter"); + initialised = true; + } + } + + /** + * Mark the node as publish in progress. This is often used as a marker to prevent any further updates + * to a node. + * @param nodeRef node reference + */ + private void markPublishInProgress(final NodeRef nodeRef) + { + RetryingTransactionHelper.RetryingTransactionCallback execution = + new RetryingTransactionHelper.RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + if (logger.isDebugEnabled() == true) + { + logger.debug("Marking updated node as publish in progress. (node=" + nodeRef.toString() + ")"); + } + + behaviourFilter.disableBehaviour(nodeRef, TYPE_DISPOSITION_ACTION_DEFINITION); + try + { + // Mark the node as publish in progress + nodeService.setProperty(nodeRef, PROP_PUBLISH_IN_PROGRESS, true); + } + finally + { + behaviourFilter.enableBehaviour(nodeRef, TYPE_DISPOSITION_ACTION_DEFINITION); + } + return null; + } + }; + retryingTransactionHelper.doInTransaction(execution, false, true); + } + + /** + * Publish the updates made to the node. + * @param nodeRef node reference + */ + private void publishUpdates(final NodeRef nodeRef) + { + RetryingTransactionHelper.RetryingTransactionCallback execution = + new RetryingTransactionHelper.RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + behaviourFilter.disableBehaviour(nodeRef, TYPE_DISPOSITION_ACTION_DEFINITION); + try + { + // Get the update to value for the node + String updateTo = (String)nodeService.getProperty(nodeRef, PROP_UPDATE_TO); + + if (updateTo != null) + { + if (logger.isDebugEnabled() == true) + { + logger.debug("Node update to " + updateTo + " (noderef=" + nodeRef.toString() + ")"); + } + + // Get the publish executor + PublishExecutor executor = register.get(updateTo); + if (executor == null) + { + if (logger.isDebugEnabled() == true) + { + logger.debug("Unable to find a corresponding publish executor. (noderef=" + nodeRef.toString() + ", updateTo=" + updateTo + ")"); + } + throw new AlfrescoRuntimeException("Unable to find a corresponding publish executor. (noderef=" + nodeRef.toString() + ", updateTo=" + updateTo + ")"); + } + + if (logger.isDebugEnabled() == true) + { + logger.debug("Attempting to publish updates. (nodeRef=" + nodeRef.toString() + ")"); + } + + // Publish + executor.publish(nodeRef); + } + else + { + if (logger.isDebugEnabled() == true) + { + logger.debug("Unable to publish, because publish executor is not set."); + } + } + + // Remove the unpublished update aspect + nodeService.removeAspect(nodeRef, ASPECT_UNPUBLISHED_UPDATE); + + if (logger.isDebugEnabled() == true) + { + logger.debug("Publish updates complete. (nodeRef=" + nodeRef.toString() + ")"); + } + } + finally + { + behaviourFilter.enableBehaviour(nodeRef, TYPE_DISPOSITION_ACTION_DEFINITION); + } + + return null; + } + }; + retryingTransactionHelper.doInTransaction(execution); + } + + /** + * Unmark node as publish in progress, assuming publish failed. + * @param nodeRef node reference + */ + private void unmarkPublishInProgress(final NodeRef nodeRef) + { + RetryingTransactionHelper.RetryingTransactionCallback execution = + new RetryingTransactionHelper.RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + behaviourFilter.disableBehaviour(nodeRef, TYPE_DISPOSITION_ACTION_DEFINITION); + try + { + // Assuming the node still has unpublished information, then unmark it in progress + if (nodeService.exists(nodeRef) == true && + nodeService.hasAspect(nodeRef, ASPECT_UNPUBLISHED_UPDATE) == true) + { + if (logger.isDebugEnabled() == true) + { + logger.debug("Removing publish in progress marker from updated node, because update was not successful. (node=" + nodeRef.toString() + ")"); + } + + nodeService.setProperty(nodeRef, PROP_PUBLISH_IN_PROGRESS, false); + } + } + finally + { + behaviourFilter.enableBehaviour(nodeRef, TYPE_DISPOSITION_ACTION_DEFINITION); + } + + return null; + } + }; + retryingTransactionHelper.doInTransaction(execution); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/job/publish/BasePublishExecutor.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/job/publish/BasePublishExecutor.java new file mode 100644 index 0000000000..f216fd3701 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/job/publish/BasePublishExecutor.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2009-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.module.org_alfresco_module_rm.job.publish; + +/** + * Base publish executor implementation + * + * @author Roy Wetherall + */ +public abstract class BasePublishExecutor implements PublishExecutor +{ + /** Publish executor registry */ + public PublishExecutorRegistry registry; + + /** + * Set publish executor registry + * @param registry publish executor registry + */ + public void setPublishExecutorRegistry(PublishExecutorRegistry registry) + { + this.registry = registry; + } + + /** + * Init method + */ + public void init() + { + registry.register(this); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/job/publish/DispositionActionDefinitionPublishExecutor.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/job/publish/DispositionActionDefinitionPublishExecutor.java new file mode 100644 index 0000000000..79a0c3abfa --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/job/publish/DispositionActionDefinitionPublishExecutor.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2009-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.module.org_alfresco_module_rm.job.publish; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService; +import org.alfresco.module.org_alfresco_module_rm.action.impl.BroadcastDispositionActionDefinitionUpdateAction; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.repo.policy.BehaviourFilter; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.QName; + +/** + * Disposition action definition publish executor + * + * @author Roy Wetherall + */ +public class DispositionActionDefinitionPublishExecutor extends BasePublishExecutor +{ + /** Node service */ + private NodeService nodeService; + + /** Records management action service */ + private RecordsManagementActionService rmActionService; + + /** Behaviour filter */ + @SuppressWarnings("unused") + private BehaviourFilter behaviourFilter; + + /** + * Set node service + * @param nodeService node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Set records management service + * @param rmActionService records management service + */ + public void setRmActionService(RecordsManagementActionService rmActionService) + { + this.rmActionService = rmActionService; + } + + /** + * Set behaviour filter + * @param behaviourFilter behaviour filter + */ + public void setBehaviourFilter(BehaviourFilter behaviourFilter) + { + this.behaviourFilter = behaviourFilter; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.job.publish.PublishExecutor#getName() + */ + @Override + public String getName() + { + return RecordsManagementModel.UPDATE_TO_DISPOSITION_ACTION_DEFINITION; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.job.publish.PublishExecutor#publish(org.alfresco.service.cmr.repository.NodeRef) + */ + @SuppressWarnings("unchecked") + @Override + public void publish(NodeRef nodeRef) + { + List updatedProps = (List)nodeService.getProperty(nodeRef, RecordsManagementModel.PROP_UPDATED_PROPERTIES); + if (updatedProps != null) + { + Map params = new HashMap(); + params.put(BroadcastDispositionActionDefinitionUpdateAction.CHANGED_PROPERTIES, (Serializable)updatedProps); + rmActionService.executeRecordsManagementAction(nodeRef, BroadcastDispositionActionDefinitionUpdateAction.NAME, params); + } + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/job/publish/PublishExecutor.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/job/publish/PublishExecutor.java new file mode 100644 index 0000000000..e2496de335 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/job/publish/PublishExecutor.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2009-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.module.org_alfresco_module_rm.job.publish; + +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Publish executor interface. + * + * @author Roy Wetherall + */ +public interface PublishExecutor +{ + /** + * @return publish exector name + */ + String getName(); + + /** + * Publish changes to node. + * + * @param nodeRef node reference + */ + void publish(NodeRef nodeRef); +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/job/publish/PublishExecutorRegistry.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/job/publish/PublishExecutorRegistry.java new file mode 100644 index 0000000000..a109bd1046 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/job/publish/PublishExecutorRegistry.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2009-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.module.org_alfresco_module_rm.job.publish; + +import java.util.HashMap; +import java.util.Map; + +/** + * Publish executor register. + * + * @author Roy Wetherall + */ +public class PublishExecutorRegistry +{ + /** Map of publish executors */ + private Map publishExectors = new HashMap(3); + + /** + * Register a publish executor + * + * @param publishExecutor publish executor + */ + public void register(PublishExecutor publishExecutor) + { + publishExectors.put(publishExecutor.getName(), publishExecutor); + } + + /** + * Get registered publish executor by name. + * + * @param name name + * @return {@link PublishExecutor}] + */ + public PublishExecutor get(String name) + { + return publishExectors.get(name); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/ScriptCapability.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/ScriptCapability.java new file mode 100644 index 0000000000..dab8b72a80 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/ScriptCapability.java @@ -0,0 +1,65 @@ +/* + * 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.module.org_alfresco_module_rm.jscript; + +/** + * @author Roy Wetherall + */ +public class ScriptCapability +{ + private String name; + private String displayLabel; + private String[] actions; + + /** + * @param name + * @param displayLabel + * @param actions + */ + protected ScriptCapability(String name, String displayLabel, String[] actions) + { + this.name = name; + this.displayLabel = displayLabel; + this.actions = actions; + } + + /** + * @return the name + */ + public String getName() + { + return name; + } + + /** + * @return the displayLabel + */ + public String getDisplayLabel() + { + return displayLabel; + } + + /** + * @return the actions + */ + public String[] getActions() + { + return actions; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/ScriptRecordsManagmentNode.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/ScriptRecordsManagmentNode.java new file mode 100644 index 0000000000..2ffb7cb2f8 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/ScriptRecordsManagmentNode.java @@ -0,0 +1,90 @@ +/* + * 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.module.org_alfresco_module_rm.jscript; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementServiceRegistry; +import org.alfresco.module.org_alfresco_module_rm.capability.Capability; +import org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService; +import org.alfresco.repo.jscript.ScriptNode; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.AccessStatus; +import org.mozilla.javascript.Scriptable; + +/** + * Base records managment script node + * + * @author Roy Wetherall + */ +public class ScriptRecordsManagmentNode extends ScriptNode +{ + private static final long serialVersionUID = 8872385533440938353L; + + private RecordsManagementServiceRegistry rmServices; + + public ScriptRecordsManagmentNode(NodeRef nodeRef, RecordsManagementServiceRegistry services, Scriptable scope) + { + super(nodeRef, services, scope); + rmServices = services; + } + + public ScriptRecordsManagmentNode(NodeRef nodeRef, RecordsManagementServiceRegistry services) + { + super(nodeRef, services); + rmServices = services; + } + + public ScriptCapability[] getCapabilities() + { + return capabilitiesSet(null); + } + + public ScriptCapability[] capabilitiesSet(String capabilitiesSet) + { + RecordsManagementSecurityService rmSecurity = rmServices.getRecordsManagementSecurityService(); + Map cMap = null; + if (capabilitiesSet == null) + { + // Get all capabilities + cMap = rmSecurity.getCapabilities(this.nodeRef); + } + else + { + cMap = rmSecurity.getCapabilities(this.nodeRef, capabilitiesSet); + } + + List list = new ArrayList(cMap.size()); + for (Map.Entry entry : cMap.entrySet()) + { + if (AccessStatus.ALLOWED.equals(entry.getValue()) == true || + AccessStatus.UNDETERMINED.equals(entry.getValue()) == true) + { + Capability cap = entry.getKey(); + String[] actions = (String[])cap.getActionNames().toArray(new String[cap.getActionNames().size()]); + ScriptCapability scriptCap = new ScriptCapability(cap.getName(), cap.getName(), actions); + list.add(scriptCap); + } + } + + return (ScriptCapability[])list.toArray(new ScriptCapability[list.size()]); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/ScriptRecordsManagmentService.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/ScriptRecordsManagmentService.java new file mode 100644 index 0000000000..c8515d457d --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/ScriptRecordsManagmentService.java @@ -0,0 +1,122 @@ +/* + * 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.module.org_alfresco_module_rm.jscript; + +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementServiceRegistry; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.module.org_alfresco_module_rm.notification.RecordsManagementNotificationHelper; +import org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService; +import org.alfresco.repo.jscript.BaseScopableProcessorExtension; +import org.alfresco.repo.jscript.ScriptNode; +import org.alfresco.scripts.ScriptException; + +/** + * Records management service + * + * @author Roy Wetherall + */ +public class ScriptRecordsManagmentService extends BaseScopableProcessorExtension + implements RecordsManagementModel +{ + /** Records management service registry */ + private RecordsManagementServiceRegistry rmServices; + + /** Records management notification helper */ + private RecordsManagementNotificationHelper notificationHelper; + + /** + * Set records management service registry + * + * @param rmServices records management service registry + */ + public void setRecordsManagementServiceRegistry(RecordsManagementServiceRegistry rmServices) + { + this.rmServices = rmServices; + } + + /** + * Sets the notification helper + * + * @param notificationHelper notification helper + */ + public void setNotificationHelper(RecordsManagementNotificationHelper notificationHelper) + { + this.notificationHelper = notificationHelper; + } + + /** + * Get records management node + * + * @param node script node + * @return ScriptRecordsManagementNode records management script node + */ + public ScriptRecordsManagmentNode getRecordsManagementNode(ScriptNode node) + { + ScriptRecordsManagmentNode result = null; + + if (rmServices.getNodeService().hasAspect(node.getNodeRef(), ASPECT_FILE_PLAN_COMPONENT) == true) + { + // TODO .. at this point determine what type of records management node is it and + // create the appropriate sub-type + result = new ScriptRecordsManagmentNode(node.getNodeRef(), rmServices); + } + else + { + throw new ScriptException("Node is not a records management node type."); + } + + return result; + } + + /** + * Set the RM permission + * + * @param node + * @param permission + * @param authority + */ + public void setPermission(ScriptNode node, String permission, String authority) + { + RecordsManagementSecurityService securityService = rmServices.getRecordsManagementSecurityService(); + securityService.setPermission(node.getNodeRef(), authority, permission); + } + + /** + * Delete the RM permission + * + * @param node + * @param permission + * @param authority + */ + public void deletePermission(ScriptNode node, String permission, String authority) + { + RecordsManagementSecurityService securityService = rmServices.getRecordsManagementSecurityService(); + securityService.deletePermission(node.getNodeRef(), authority, permission); + } + + /** + * Send superseded notification + * + * @param record superseded record + */ + public void sendSupersededNotification(ScriptNode record) + { + notificationHelper.recordSupersededEmailNotification(record.getNodeRef()); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/BaseEvaluator.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/BaseEvaluator.java new file mode 100644 index 0000000000..f1d3a15d22 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/BaseEvaluator.java @@ -0,0 +1,233 @@ +/* + * 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.module.org_alfresco_module_rm.jscript.app; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.module.org_alfresco_module_rm.FilePlanComponentKind; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.capability.Capability; +import org.alfresco.module.org_alfresco_module_rm.capability.CapabilityService; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +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.namespace.NamespaceService; + +/** + * Base evaluator. + * + * @author Roy Wetherall + */ +public abstract class BaseEvaluator implements RecordsManagementModel +{ + /** Name */ + protected String name; + + /** JSON conversion component */ + protected JSONConversionComponent jsonConversionComponent; + + /** Records management service */ + protected RecordsManagementService recordsManagementService; + + /** Node service */ + protected NodeService nodeService; + + /** Namespace service */ + protected NamespaceService namespaceService; + + /** Capability service */ + protected CapabilityService capabilityService; + + /** File plan component kinds */ + protected Set kinds; + + /** Capabilities */ + protected List capabilities; + + /** + * @param jsonConversionComponent json conversion component + */ + public void setJsonConversionComponent(JSONConversionComponent jsonConversionComponent) + { + this.jsonConversionComponent = jsonConversionComponent; + } + + /** + * @param recordsManagementService records management service + */ + public void setRecordsManagementService(RecordsManagementService recordsManagementService) + { + this.recordsManagementService = recordsManagementService; + } + + /** + * @param nodeService node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * @param namespaceService namespace service + */ + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + /** + * @param capabilityService capability service + */ + public void setCapabilityService(CapabilityService capabilityService) + { + this.capabilityService = capabilityService; + } + + /** + * @param name + */ + public void setName(String name) + { + this.name = name; + } + + /** + * @return + */ + public String getName() + { + return this.name; + } + + /** + * @param kinds + */ + public void setKinds(Set kinds) + { + this.kinds = kinds; + } + + /** + * @param capabilties + */ + public void setCapabilities(List capabilties) + { + this.capabilities = capabilties; + } + + /** + * Helper method which sets on capability. + * + * @param capability capability name + */ + public void setCapability(String capability) + { + List list = new ArrayList(1); + list.add(capability); + this.capabilities = list; + } + + /** + * Registers this instance as an indicator (evaluator) + */ + public void registerIndicator() + { + jsonConversionComponent.registerIndicator(this); + } + + /** + * Registers this instance as an action (evaluator) + */ + public void registerAction() + { + jsonConversionComponent.registerAction(this); + } + + /** + * Executes the evaluation. + * + * @param nodeRef + * @return + */ + public boolean evaluate(NodeRef nodeRef) + { + boolean result = false; + + // Check that we are dealing with the correct kind of RM object + if (kinds == null || checkKinds(nodeRef) == true) + { + // Check we have the required capabilities + if (capabilities == null || checkCapabilities(nodeRef) == true) + { + result = evaluateImpl(nodeRef); + } + } + + return result; + } + + /** + * Checks the file plan component kind. + * + * @param nodeRef + * @return + */ + private boolean checkKinds(NodeRef nodeRef) + { + FilePlanComponentKind kind = recordsManagementService.getFilePlanComponentKind(nodeRef); + return kinds.contains(kind); + } + + /** + * Checks the capabilities. + * + * @param nodeRef + * @return + */ + private boolean checkCapabilities(NodeRef nodeRef) + { + boolean result = true; + if (capabilities != null && capabilities.isEmpty() == false) + { + Map accessStatus = capabilityService.getCapabilitiesAccessState(nodeRef, capabilities); + for (AccessStatus value : accessStatus.values()) + { + if (AccessStatus.ALLOWED.equals(value) == false) + { + result = false; + break; + } + } + } + return result; + } + + /** + * Evaluation execution implementation. + * + * @param nodeRef + * @return + */ + protected abstract boolean evaluateImpl(NodeRef nodeRef); +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/JSONConversionComponent.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/JSONConversionComponent.java new file mode 100644 index 0000000000..03ebff03d8 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/JSONConversionComponent.java @@ -0,0 +1,233 @@ +/* + * 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.module.org_alfresco_module_rm.jscript.app; + +import java.util.ArrayList; +import java.util.List; + +import org.alfresco.module.org_alfresco_module_rm.FilePlanComponentKind; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.capability.RMPermissionModel; +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.AccessStatus; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; + +/** + * Extend JSON conversion component to include RM specifics. + * + * @author Roy Wetherall + */ +public class JSONConversionComponent extends org.alfresco.repo.jscript.app.JSONConversionComponent +{ + /** Records management service */ + private RecordsManagementService recordsManagementService; + + /** Indicators */ + private List indicators = new ArrayList(); + + /** Actions */ + private List actions = new ArrayList(); + + /** + * @param recordsManagementService records management service + */ + public void setRecordsManagementService(RecordsManagementService recordsManagementService) + { + this.recordsManagementService = recordsManagementService; + } + + /** + * @param indicator registered indicator + */ + public void registerIndicator(BaseEvaluator indicator) + { + indicators.add(indicator); + } + + /** + * @param action registered action + */ + public void registerAction(BaseEvaluator action) + { + actions.add(action); + } + + /** + * @see org.alfresco.repo.jscript.app.JSONConversionComponent#setRootValues(org.alfresco.service.cmr.model.FileInfo, org.json.simple.JSONObject, boolean) + */ + @SuppressWarnings("unchecked") + @Override + protected void setRootValues(FileInfo nodeInfo, JSONObject rootJSONObject, boolean useShortQNames) + { + // Set the base root values + super.setRootValues(nodeInfo, rootJSONObject, useShortQNames); + + // Get the node reference for convenience + NodeRef nodeRef = nodeInfo.getNodeRef(); + + if (AccessStatus.ALLOWED.equals(permissionService.hasPermission(nodeRef, RMPermissionModel.READ_RECORDS)) == true) + { + // Indicate whether the node is a RM object or not + boolean isFilePlanComponent = recordsManagementService.isFilePlanComponent(nodeInfo.getNodeRef()); + rootJSONObject.put("isRmNode", isFilePlanComponent); + + if (isFilePlanComponent == true) + { + rootJSONObject.put("rmNode", setRmNodeValues(nodeRef, rootJSONObject, useShortQNames)); + } + } + } + + /** + * + * @param nodeRef + * @param rootJSONObject + * @param useShortQName + * @return + */ + @SuppressWarnings("unchecked") + private JSONObject setRmNodeValues(NodeRef nodeRef, JSONObject rootJSONObject, boolean useShortQName) + { + JSONObject rmNodeValues = new JSONObject(); + + // UI convenience type + rmNodeValues.put("uiType", getUIType(nodeRef)); + + // Get the 'kind' of the file plan component + FilePlanComponentKind kind = recordsManagementService.getFilePlanComponentKind(nodeRef); + rmNodeValues.put("kind", kind.toString()); + + // Set the indicators array + setIndicators(rmNodeValues, nodeRef); + + // Set the actions array + setActions(rmNodeValues, nodeRef); + + return rmNodeValues; + } + + @SuppressWarnings("unchecked") + private void setIndicators(JSONObject rmNodeValues, NodeRef nodeRef) + { + if (indicators != null && indicators.isEmpty() == false) + { + JSONArray jsonIndicators = new JSONArray(); + + for (BaseEvaluator indicator : indicators) + { + if (indicator.evaluate(nodeRef) == true) + { + jsonIndicators.add(indicator.getName()); + } + } + + rmNodeValues.put("indicators", jsonIndicators); + } + } + + @SuppressWarnings("unchecked") + private void setActions(JSONObject rmNodeValues, NodeRef nodeRef) + { + if (actions != null && actions.isEmpty() == false) + { + JSONArray jsonActions = new JSONArray(); + + for (BaseEvaluator action : actions) + { + if (action.evaluate(nodeRef) == true) + { + jsonActions.add(action.getName()); + } + } + + rmNodeValues.put("actions", jsonActions); + } + } + + /** + * Gets the rm 'type' used as a UI convenience and compatibility flag. + */ + private String getUIType(NodeRef nodeRef) + { + String result = "unknown"; + + FilePlanComponentKind kind = recordsManagementService.getFilePlanComponentKind(nodeRef); + if (kind != null) + { + switch (kind) + { + case FILE_PLAN: + { + result = "fileplan"; + break; + } + case RECORD_CATEGORY: + { + result = "record-category"; + break; + } + case RECORD_FOLDER: + { + if (recordsManagementService.isMetadataStub(nodeRef) == true) + { + result = "metadata-stub-folder"; + } + else + { + result = "record-folder"; + } + break; + } + case RECORD: + { + if (recordsManagementService.isMetadataStub(nodeRef) == true) + { + result = "metadata-stub"; + } + else + { + if (recordsManagementService.isRecordDeclared(nodeRef) == true) + { + result = "record"; + } + else + { + result = "undeclared-record"; + } + } + break; + } + case HOLD: + { + result = "hold-container"; + break; + } + case TRANSFER: + { + result = "transfer-container"; + break; + } + } + } + + return result; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/evaluator/CutoffEvaluator.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/evaluator/CutoffEvaluator.java new file mode 100644 index 0000000000..0733bf7e16 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/evaluator/CutoffEvaluator.java @@ -0,0 +1,43 @@ +/* + * 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.module.org_alfresco_module_rm.jscript.app.evaluator; + +import org.alfresco.module.org_alfresco_module_rm.jscript.app.BaseEvaluator; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Cutoff indicator + * + * @author Roy Wetherall + */ +public class CutoffEvaluator extends BaseEvaluator +{ + private boolean isCutoff = true; + + public void setCutoff(boolean isCutoff) + { + this.isCutoff = isCutoff; + } + + @Override + protected boolean evaluateImpl(NodeRef nodeRef) + { + return (recordsManagementService.isCutoff(nodeRef) == isCutoff); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/evaluator/FolderOpenClosedEvaluator.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/evaluator/FolderOpenClosedEvaluator.java new file mode 100644 index 0000000000..7855ee3b89 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/evaluator/FolderOpenClosedEvaluator.java @@ -0,0 +1,41 @@ +/* + * 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.module.org_alfresco_module_rm.jscript.app.evaluator; + +import org.alfresco.module.org_alfresco_module_rm.jscript.app.BaseEvaluator; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * @author Roy Wetherall + */ +public class FolderOpenClosedEvaluator extends BaseEvaluator +{ + private boolean expected = true; + + public void setExpected(boolean expected) + { + this.expected = expected; + } + + @Override + protected boolean evaluateImpl(NodeRef nodeRef) + { + return (recordsManagementService.isRecordFolderClosed(nodeRef) == expected); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/evaluator/FrozenEvaluator.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/evaluator/FrozenEvaluator.java new file mode 100644 index 0000000000..3159c746b0 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/evaluator/FrozenEvaluator.java @@ -0,0 +1,34 @@ +/* + * 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.module.org_alfresco_module_rm.jscript.app.evaluator; + +import org.alfresco.module.org_alfresco_module_rm.jscript.app.BaseEvaluator; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * @author Roy Wetherall + */ +public class FrozenEvaluator extends BaseEvaluator +{ + @Override + protected boolean evaluateImpl(NodeRef nodeRef) + { + return recordsManagementService.isFrozen(nodeRef); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/evaluator/HasAspectEvaluator.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/evaluator/HasAspectEvaluator.java new file mode 100644 index 0000000000..c95f793e9f --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/evaluator/HasAspectEvaluator.java @@ -0,0 +1,45 @@ +/* + * 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.module.org_alfresco_module_rm.jscript.app.evaluator; + +import org.alfresco.module.org_alfresco_module_rm.jscript.app.BaseEvaluator; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + +/** + * Transfered indicator + * + * @author Roy Wetherall + */ +public class HasAspectEvaluator extends BaseEvaluator +{ + private String prefixAspectQNameString; + + public void setAspect(String aspect) + { + prefixAspectQNameString = aspect; + } + + @Override + protected boolean evaluateImpl(NodeRef nodeRef) + { + QName aspect = QName.createQName(prefixAspectQNameString, namespaceService); + return nodeService.hasAspect(nodeRef, aspect); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/evaluator/MultiParentEvaluator.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/evaluator/MultiParentEvaluator.java new file mode 100644 index 0000000000..805a082711 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/evaluator/MultiParentEvaluator.java @@ -0,0 +1,40 @@ +/* + * 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.module.org_alfresco_module_rm.jscript.app.evaluator; + +import java.util.List; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.jscript.app.BaseEvaluator; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.RegexQNamePattern; + +/** + * @author Roy Wetherall + */ +public class MultiParentEvaluator extends BaseEvaluator +{ + @Override + protected boolean evaluateImpl(NodeRef nodeRef) + { + List parents = nodeService.getParentAssocs(nodeRef, ContentModel.ASSOC_CONTAINS, RegexQNamePattern.MATCH_ALL); + return (parents.size() > 1); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/evaluator/NonElectronicEvaluator.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/evaluator/NonElectronicEvaluator.java new file mode 100644 index 0000000000..2c88c5d0ac --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/evaluator/NonElectronicEvaluator.java @@ -0,0 +1,49 @@ +/* + * 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.module.org_alfresco_module_rm.jscript.app.evaluator; + +import org.alfresco.module.org_alfresco_module_rm.jscript.app.BaseEvaluator; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + +/** + * @author Roy Wetherall + */ +public class NonElectronicEvaluator extends BaseEvaluator +{ + private DictionaryService dictionaryService; + + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + @Override + protected boolean evaluateImpl(NodeRef nodeRef) + { + boolean result = false; + QName qName = nodeService.getType(nodeRef); + if (qName != null && dictionaryService.isSubClass(qName, TYPE_NON_ELECTRONIC_DOCUMENT) == true) + { + result = true; + } + return result; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/evaluator/SplitEmailActionEvaluator.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/evaluator/SplitEmailActionEvaluator.java new file mode 100644 index 0000000000..c28f1ae23b --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/evaluator/SplitEmailActionEvaluator.java @@ -0,0 +1,54 @@ +/* + * 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.module.org_alfresco_module_rm.jscript.app.evaluator; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.jscript.app.BaseEvaluator; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Split EMail action evaluator + * + * @author Roy Wetherall + */ +public class SplitEmailActionEvaluator extends BaseEvaluator +{ + @Override + protected boolean evaluateImpl(NodeRef nodeRef) + { + boolean result = false; + if (recordsManagementService.isRecordDeclared(nodeRef) == false) + { + ContentData contentData = (ContentData)nodeService.getProperty(nodeRef, ContentModel.PROP_CONTENT); + if (contentData != null) + { + String mimetype = contentData.getMimetype(); + if (mimetype != null && + (MimetypeMap.MIMETYPE_RFC822.equals(mimetype) == true || + MimetypeMap.MIMETYPE_OUTLOOK_MSG.equals(mimetype) == true)) + { + result = true; + } + } + } + return result; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/evaluator/TransferEvaluator.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/evaluator/TransferEvaluator.java new file mode 100644 index 0000000000..8a22f8085a --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/evaluator/TransferEvaluator.java @@ -0,0 +1,55 @@ +/* + * 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.module.org_alfresco_module_rm.jscript.app.evaluator; + +import java.util.List; + +import org.alfresco.module.org_alfresco_module_rm.jscript.app.BaseEvaluator; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.RegexQNamePattern; + +/** + * @author Roy Wetherall + */ +public class TransferEvaluator extends BaseEvaluator +{ + private boolean transferAccessionIndicator = false; + + public void setTransferAccessionIndicator(boolean transferAccessionIndicator) + { + this.transferAccessionIndicator = transferAccessionIndicator; + } + + @Override + protected boolean evaluateImpl(NodeRef nodeRef) + { + List parents = nodeService.getParentAssocs(nodeRef, RecordsManagementModel.ASSOC_TRANSFERRED, RegexQNamePattern.MATCH_ALL); + if (parents.size() == 1) + { + boolean actual = ((Boolean)nodeService.getProperty(parents.get(0).getParentRef(), RecordsManagementModel.PROP_TRANSFER_ACCESSION_INDICATOR)).booleanValue(); + return (actual == transferAccessionIndicator); + } + else + { + return false; + } + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/evaluator/TrueEvaluator.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/evaluator/TrueEvaluator.java new file mode 100644 index 0000000000..e559216b3b --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/evaluator/TrueEvaluator.java @@ -0,0 +1,36 @@ +/* + * 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.module.org_alfresco_module_rm.jscript.app.evaluator; + +import org.alfresco.module.org_alfresco_module_rm.jscript.app.BaseEvaluator; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Cutoff indicator + * + * @author Roy Wetherall + */ +public class TrueEvaluator extends BaseEvaluator +{ + @Override + protected boolean evaluateImpl(NodeRef nodeRef) + { + return true; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/evaluator/VitalRecordEvaluator.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/evaluator/VitalRecordEvaluator.java new file mode 100644 index 0000000000..65c253976a --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/evaluator/VitalRecordEvaluator.java @@ -0,0 +1,42 @@ +/* + * 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.module.org_alfresco_module_rm.jscript.app.evaluator; + +import org.alfresco.module.org_alfresco_module_rm.jscript.app.BaseEvaluator; +import org.alfresco.module.org_alfresco_module_rm.vital.VitalRecordService; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * @author Roy Wetherall + */ +public class VitalRecordEvaluator extends BaseEvaluator +{ + private VitalRecordService vitalRecordService; + + public void setVitalRecordService(VitalRecordService vitalRecordService) + { + this.vitalRecordService = vitalRecordService; + } + + @Override + protected boolean evaluateImpl(NodeRef nodeRef) + { + return vitalRecordService.isVitalRecord(nodeRef); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/CustomisableTypesBootstrap.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/CustomisableTypesBootstrap.java new file mode 100644 index 0000000000..d9f6e5722b --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/CustomisableTypesBootstrap.java @@ -0,0 +1,78 @@ +/* + * 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.module.org_alfresco_module_rm.model; + +import java.util.List; + +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; + +/** + * Bootstrap bean that indicates that the specified types or aspects are + * + * @author Roy Wetherall + */ +public class CustomisableTypesBootstrap +{ + /** Records management admin service */ + private RecordsManagementAdminService recordsManagementAdminService; + + /** Namespace service */ + private NamespaceService namespaceService; + + /** List of types and aspects to register as customisable */ + private List customisable; + + /** + * @param recordsManagementAdminService records management admin service + */ + public void setRecordsManagementAdminService(RecordsManagementAdminService recordsManagementAdminService) + { + this.recordsManagementAdminService = recordsManagementAdminService; + } + + /** + * @param namespaceService namespace service + */ + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + /** + * @param customizable list of types and aspects to register as customisable + */ + public void setCustomisable(List customisable) + { + this.customisable = customisable; + } + + /** + * Bean initialisation method + */ + public void init() + { + for (String customType : customisable) + { + QName customTypeQName = QName.createQName(customType, namespaceService); + recordsManagementAdminService.makeCustomisable(customTypeQName); + } + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/FilePlanComponentAspect.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/FilePlanComponentAspect.java new file mode 100644 index 0000000000..637f0caa4d --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/FilePlanComponentAspect.java @@ -0,0 +1,205 @@ +/* + * 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.module.org_alfresco_module_rm.model; + +import java.io.Serializable; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.repo.copy.AbstractCopyBehaviourCallback; +import org.alfresco.repo.copy.CopyBehaviourCallback; +import org.alfresco.repo.copy.CopyDetails; +import org.alfresco.repo.node.NodeServicePolicies; +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.policy.Behaviour.NotificationFrequency; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +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.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; + +/** + * Behaviour associated with the file plan component aspect + * + * @author Roy Wetherall + */ +public class FilePlanComponentAspect implements RecordsManagementModel, + NodeServicePolicies.OnAddAspectPolicy, + NodeServicePolicies.OnMoveNodePolicy +{ + /** Policy component */ + private PolicyComponent policyComponent; + + /** Records Management Service */ + private RecordsManagementService recordsManagementService; + + /** Node service */ + private NodeService nodeService; + + /** + * Set the policy component + * @param policyComponent policy component + */ + public void setPolicyComponent(PolicyComponent policyComponent) + { + this.policyComponent = policyComponent; + } + + /** + * Set the records management service + * @param recordsManagementService records management service + */ + public void setRecordsManagementService(RecordsManagementService recordsManagementService) + { + this.recordsManagementService = recordsManagementService; + } + + /** + * Set node service + * @param nodeService node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Bean initialisation method + */ + public void init() + { + policyComponent.bindClassBehaviour( + NodeServicePolicies.OnAddAspectPolicy.QNAME, + ASPECT_FILE_PLAN_COMPONENT, + new JavaBehaviour(this, "onAddAspect", NotificationFrequency.TRANSACTION_COMMIT)); + policyComponent.bindClassBehaviour( + NodeServicePolicies.OnMoveNodePolicy.QNAME, + ASPECT_FILE_PLAN_COMPONENT, + new JavaBehaviour(this, "onMoveNode", NotificationFrequency.TRANSACTION_COMMIT)); + this.policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "getCopyCallback"), + ASPECT_FILE_PLAN_COMPONENT, + new JavaBehaviour(this, "getCopyCallback")); + } + + /** + * @see org.alfresco.repo.node.NodeServicePolicies.OnAddAspectPolicy#onAddAspect(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName) + */ + @Override + public void onAddAspect(final NodeRef nodeRef, final QName aspectTypeQName) + { + AuthenticationUtil.runAs(new RunAsWork() + { + @Override + public Void doWork() throws Exception + { + if (nodeService.exists(nodeRef) == true) + { + // Look up the root and set on the aspect if found + NodeRef root = recordsManagementService.getFilePlan(nodeRef); + if (root != null) + { + nodeService.setProperty(nodeRef, PROP_ROOT_NODEREF, root); + } + } + + return null; + } + }, AuthenticationUtil.getSystemUserName()); + } + + /** + * @see org.alfresco.repo.node.NodeServicePolicies.OnMoveNodePolicy#onMoveNode(org.alfresco.service.cmr.repository.ChildAssociationRef, org.alfresco.service.cmr.repository.ChildAssociationRef) + */ + @Override + public void onMoveNode(final ChildAssociationRef oldChildAssocRef, final ChildAssociationRef newChildAssocRef) + { + AuthenticationUtil.runAs(new RunAsWork() + { + @Override + public Void doWork() throws Exception + { + if (nodeService.exists(newChildAssocRef.getParentRef()) == true && + nodeService.exists(newChildAssocRef.getChildRef()) == true) + { + // Look up the root and re-set the value currently stored on the aspect + NodeRef root = recordsManagementService.getFilePlan(newChildAssocRef.getParentRef()); + // NOTE: set the null value if no root found + nodeService.setProperty(newChildAssocRef.getChildRef(), PROP_ROOT_NODEREF, root); + } + + return null; + } + }, AuthenticationUtil.getSystemUserName()); + } + + /** + * Copy behaviour call back + * + * @param classRef class reference + * @param copyDetail details of the information being copied + * @return CopyBehaviourCallback + */ + public CopyBehaviourCallback getCopyCallback(QName classRef, CopyDetails copyDetails) + { + return new AbstractCopyBehaviourCallback() + { + /** + * @see org.alfresco.repo.copy.CopyBehaviourCallback#getChildAssociationCopyAction(org.alfresco.service.namespace.QName, org.alfresco.repo.copy.CopyDetails, org.alfresco.repo.copy.CopyBehaviourCallback.CopyChildAssociationDetails) + */ + public ChildAssocCopyAction getChildAssociationCopyAction( + QName classQName, + CopyDetails copyDetails, + CopyChildAssociationDetails childAssocCopyDetails) + { + // Do not copy the associations + return null; + } + + /** + * @see org.alfresco.repo.copy.CopyBehaviourCallback#getCopyProperties(org.alfresco.service.namespace.QName, org.alfresco.repo.copy.CopyDetails, java.util.Map) + */ + public Map getCopyProperties( + QName classQName, + CopyDetails copyDetails, + Map properties) + { + // Only copy the root node reference if the new value can be looked up via the parent + NodeRef root = recordsManagementService.getFilePlan(copyDetails.getTargetParentNodeRef()); + if (root != null) + { + properties.put(PROP_ROOT_NODEREF, root); + } + return properties; + } + + /** + * @see org.alfresco.repo.copy.CopyBehaviourCallback#getMustCopy(org.alfresco.service.namespace.QName, org.alfresco.repo.copy.CopyDetails) + */ + public boolean getMustCopy(QName classQName, CopyDetails copyDetails) + { + // Ensure the aspect is copied + return true; + } + }; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordComponentIdentifierAspect.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordComponentIdentifierAspect.java new file mode 100644 index 0000000000..d2589821b2 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordComponentIdentifierAspect.java @@ -0,0 +1,172 @@ +/* + * 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.module.org_alfresco_module_rm.model; + +import java.io.Serializable; +import java.util.Map; + +import org.alfresco.repo.node.NodeServicePolicies; +import org.alfresco.repo.node.NodeServicePolicies.BeforeDeleteNodePolicy; +import org.alfresco.repo.node.NodeServicePolicies.OnUpdatePropertiesPolicy; +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.policy.Behaviour.NotificationFrequency; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.service.cmr.attributes.AttributeService; +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.namespace.QName; +import org.alfresco.util.PropertyCheck; + +/** + * Record component identifier aspect behaviour + * + * @author Roy Wetherall + */ +public class RecordComponentIdentifierAspect + implements NodeServicePolicies.OnUpdatePropertiesPolicy, + NodeServicePolicies.BeforeDeleteNodePolicy, + RecordsManagementModel +{ + private static final String CONTEXT_VALUE = "rma:identifier"; + + private PolicyComponent policyComponent; + private NodeService nodeService; + private AttributeService attributeService; + + /** + * @param policyComponent the policyComponent to set + */ + public void setPolicyComponent(PolicyComponent policyComponent) + { + this.policyComponent = policyComponent; + } + + /** + * @param nodeService the nodeService to set + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Set the component to manage the unique properties + */ + public void setAttributeService(AttributeService attributeService) + { + this.attributeService = attributeService; + } + + /** + * Initialise method + */ + public void init() + { + PropertyCheck.mandatory(this, "policyComponent", policyComponent); + PropertyCheck.mandatory(this, "nodeService", nodeService); + PropertyCheck.mandatory(this, "attributeService", attributeService); + + policyComponent.bindClassBehaviour( + OnUpdatePropertiesPolicy.QNAME, + ASPECT_RECORD_COMPONENT_ID, + new JavaBehaviour(this, "onUpdateProperties", NotificationFrequency.EVERY_EVENT)); + policyComponent.bindClassBehaviour( + BeforeDeleteNodePolicy.QNAME, + ASPECT_RECORD_COMPONENT_ID, + new JavaBehaviour(this, "beforeDeleteNode", NotificationFrequency.EVERY_EVENT)); + } + + /** + * Ensures that the {@link RecordsManagementModel#PROP_IDENTIFIER rma:identifier} property remains + * unique within the context of the parent node. + */ + public void onUpdateProperties(final NodeRef nodeRef, final Map before, final Map after) + { + AuthenticationUtil.runAs(new RunAsWork() + { + public Object doWork() + { + // Check whether the identifier property has changed + String beforeId = (String)before.get(PROP_IDENTIFIER); + String afterId = (String)after.get(PROP_IDENTIFIER); + updateUniqueness(nodeRef, beforeId, afterId); + return null; + } + }, AuthenticationUtil.getSystemUserName()); + } + + /** + * Cleans up the {@link RecordsManagementModel#PROP_IDENTIFIER rma:identifier} property unique triplet. + */ + public void beforeDeleteNode(final NodeRef nodeRef) + { + AuthenticationUtil.runAs(new RunAsWork() + { + public Object doWork() + { + String beforeId = (String) nodeService.getProperty(nodeRef, PROP_IDENTIFIER); + updateUniqueness(nodeRef, beforeId, null); + return null; + } + }, AuthenticationUtil.getSystemUserName()); + } + + /** + * Updates the uniqueness check using the values provided. If the after value is null + * then this is considered to be a removal. + */ + private void updateUniqueness(NodeRef nodeRef, String beforeId, String afterId) + { + ChildAssociationRef childAssoc = nodeService.getPrimaryParent(nodeRef); + NodeRef contextNodeRef = childAssoc.getParentRef(); + + if (beforeId == null) + { + if (afterId != null) + { + // Just create it + attributeService.createAttribute(null, CONTEXT_VALUE, contextNodeRef, afterId); + } + else + { + // This happens if the unique property is not present + } + } + else if (afterId == null) + { + if (beforeId != null) + { + // The before value was not null, so remove it + attributeService.removeAttribute(CONTEXT_VALUE, contextNodeRef, beforeId); + } + // Do a blanket removal in case this is a contextual nodes + attributeService.removeAttributes(CONTEXT_VALUE, nodeRef); + } + else + { + // This is a full update + attributeService.updateOrCreateAttribute( + CONTEXT_VALUE, contextNodeRef, beforeId, + CONTEXT_VALUE, contextNodeRef, afterId); + } + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordContainerType.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordContainerType.java new file mode 100644 index 0000000000..747a7647ce --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordContainerType.java @@ -0,0 +1,191 @@ +/* + * 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.module.org_alfresco_module_rm.model; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService; +import org.alfresco.module.org_alfresco_module_rm.identifier.IdentifierService; +import org.alfresco.repo.node.NodeServicePolicies; +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.policy.Behaviour.NotificationFrequency; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.service.cmr.dictionary.DictionaryService; +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.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; + +/** + * Behaviour associated with the record container type + * + * @author Roy Wetherall + */ +public class RecordContainerType implements RecordsManagementModel, + NodeServicePolicies.OnCreateChildAssociationPolicy, + NodeServicePolicies.OnCreateNodePolicy +{ + /** Policy component */ + private PolicyComponent policyComponent; + + /** Records Management Action Service */ + private RecordsManagementActionService recordsManagementActionService; + + /** Node service */ + private NodeService nodeService; + + /** Dictionary service */ + private DictionaryService dictionaryService; + + /** Identity service */ + private IdentifierService recordsManagementIdentifierService; + + /** + * Set the policy component + * @param policyComponent policy component + */ + public void setPolicyComponent(PolicyComponent policyComponent) + { + this.policyComponent = policyComponent; + } + + /** + * Set the records management action service + * + * @param recordsManagementActionService records management action service + */ + public void setRecordsManagementActionService(RecordsManagementActionService recordsManagementActionService) + { + this.recordsManagementActionService = recordsManagementActionService; + } + + /** + * Set node service + * + * @param nodeService node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Set dictionary service + * + * @param dictionaryService dictionary serviceS + */ + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + /** + * Set the identity service + * + * @param recordsManagementIdentifierService identity service + */ + public void setRecordsManagementIdentifierService(IdentifierService recordsManagementIdentifierService) + { + this.recordsManagementIdentifierService = recordsManagementIdentifierService; + } + + /** + * Bean initialisation method + */ + public void init() + { + this.policyComponent.bindAssociationBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onCreateChildAssociation"), + TYPE_RECORDS_MANAGEMENT_CONTAINER, + ContentModel.ASSOC_CONTAINS, + new JavaBehaviour(this, "onCreateChildAssociation", NotificationFrequency.TRANSACTION_COMMIT)); + this.policyComponent.bindClassBehaviour( + NodeServicePolicies.OnCreateNodePolicy.QNAME, + TYPE_FILE_PLAN, + new JavaBehaviour(this, "onCreateNode", NotificationFrequency.TRANSACTION_COMMIT)); + } + + /** + * Deal with something created within a record container + */ + @Override + public void onCreateChildAssociation(ChildAssociationRef childAssocRef, boolean isNewNode) + { + // Get the elements of the created association + final NodeRef child = childAssocRef.getChildRef(); + QName childType = nodeService.getType(child); + + // We only care about "folder" or sub-types + if (dictionaryService.isSubClass(childType, ContentModel.TYPE_FOLDER) == true) + { + // We need to automatically cast the created folder to RM type if it is a plain folder + // This occurs if the RM folder has been created via IMap, WebDav, etc + if (nodeService.hasAspect(child, ASPECT_FILE_PLAN_COMPONENT) == false) + { + // TODO it may not always be a record folder ... perhaps if the current user is a admin it would be a record category?? + + // Assume any created folder is a rma:recordFolder + nodeService.setType(child, TYPE_RECORD_FOLDER); + } + + if (TYPE_RECORD_FOLDER.equals(nodeService.getType(child)) == true) + { + // Setup record folder + recordsManagementActionService.executeRecordsManagementAction(child, "setupRecordFolder"); + } + + // Catch all to generate the rm id (assuming it doesn't already have one!) + setIdenifierProperty(child); + + } + } + + /** + * @see org.alfresco.repo.node.NodeServicePolicies.OnCreateNodePolicy#onCreateNode(org.alfresco.service.cmr.repository.ChildAssociationRef) + */ + @Override + public void onCreateNode(ChildAssociationRef childAssocRef) + { + // When a new root container is created, make sure the identifier is set + setIdenifierProperty(childAssocRef.getChildRef()); + } + + /** + * + * @param nodeRef + */ + private void setIdenifierProperty(final NodeRef nodeRef) + { + AuthenticationUtil.runAs(new RunAsWork() + { + public Object doWork() throws Exception + { + if (nodeService.hasAspect(nodeRef, ASPECT_FILE_PLAN_COMPONENT) == true && + nodeService.getProperty(nodeRef, PROP_IDENTIFIER) == null) + { + String id = recordsManagementIdentifierService.generateIdentifier(nodeRef); + nodeService.setProperty(nodeRef, RecordsManagementModel.PROP_IDENTIFIER, id); + } + return null; + } + }, AuthenticationUtil.getAdminUserName()); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordCopyBehaviours.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordCopyBehaviours.java new file mode 100644 index 0000000000..7c4377c9d0 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordCopyBehaviours.java @@ -0,0 +1,253 @@ +/* + * 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.module.org_alfresco_module_rm.model; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementServiceRegistry; +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService; +import org.alfresco.repo.copy.AbstractCopyBehaviourCallback; +import org.alfresco.repo.copy.CopyBehaviourCallback; +import org.alfresco.repo.copy.CopyDetails; +import org.alfresco.repo.copy.DoNothingCopyBehaviourCallback; +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.policy.Behaviour.NotificationFrequency; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +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.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; + +/** + * Class containing behaviour for the vitalRecordDefinition aspect. + * + * @author neilm + */ +public class RecordCopyBehaviours implements RecordsManagementModel +{ + /** The policy component */ + private PolicyComponent policyComponent; + + /** The rm service registry */ + private RecordsManagementServiceRegistry rmServiceRegistry; + + /** List of aspects to remove during move and copy */ + private List unwantedAspects = new ArrayList(5); + + /** + * Set the policy component + * + * @param policyComponent the policy component + */ + public void setPolicyComponent(PolicyComponent policyComponent) + { + this.policyComponent = policyComponent; + } + + /** + * Set the rm service registry. + * + * @param recordsManagementServiceRegistry the rm service registry. + */ + public void setRecordsManagementServiceRegistry(RecordsManagementServiceRegistry recordsManagementServiceRegistry) + { + this.rmServiceRegistry = recordsManagementServiceRegistry; + } + + /** + * Initialise the vitalRecord aspect policies + */ + public void init() + { + // Set up list of unwanted aspects + unwantedAspects.add(ASPECT_VITAL_RECORD); + unwantedAspects.add(ASPECT_DISPOSITION_LIFECYCLE); + unwantedAspects.add(RecordsManagementSearchBehaviour.ASPECT_RM_SEARCH); + + // Do not copy any of the Alfresco-internal 'state' aspects + for (QName aspect : unwantedAspects) + { + this.policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "getCopyCallback"), + aspect, + new JavaBehaviour(this, "getDoNothingCopyCallback")); + } + this.policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "getCopyCallback"), + ASPECT_RECORD_COMPONENT_ID, + new JavaBehaviour(this, "getIdCallback")); + + // Move behaviour + this.policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onMoveNode"), + RecordsManagementModel.ASPECT_RECORD, + new JavaBehaviour(this, "onMoveRecordNode", NotificationFrequency.FIRST_EVENT)); + this.policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onMoveNode"), + RecordsManagementModel.TYPE_RECORD_FOLDER, + new JavaBehaviour(this, "onMoveRecordFolderNode", NotificationFrequency.FIRST_EVENT)); + } + + /** + * onMove record behaviour + * + * @param oldChildAssocRef + * @param newChildAssocRef + */ + public void onMoveRecordNode(ChildAssociationRef oldChildAssocRef, ChildAssociationRef newChildAssocRef) + { + final NodeRef newNodeRef = newChildAssocRef.getChildRef(); + final NodeService nodeService = rmServiceRegistry.getNodeService(); + + AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public Object doWork() throws Exception + { + if (nodeService.exists(newNodeRef) == true) + { + // Remove unwanted aspects + removeUnwantedAspects(nodeService, newNodeRef); + } + + return null; + } + }, AuthenticationUtil.getAdminUserName()); + } + + /** + * onMove record folder behaviour + * + * @param oldChildAssocRef + * @param newChildAssocRef + */ + public void onMoveRecordFolderNode(ChildAssociationRef oldChildAssocRef, ChildAssociationRef newChildAssocRef) + { + final NodeRef newNodeRef = newChildAssocRef.getChildRef(); + final NodeService nodeService = rmServiceRegistry.getNodeService(); + final RecordsManagementService rmService = rmServiceRegistry.getRecordsManagementService(); + final RecordsManagementActionService rmActionService = rmServiceRegistry.getRecordsManagementActionService(); + + AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public Object doWork() throws Exception + { + if (nodeService.exists(newNodeRef) == true) + { + // Remove unwanted aspects + removeUnwantedAspects(nodeService, newNodeRef); + + // Trigger folder setup + rmActionService.executeRecordsManagementAction(newNodeRef, "setupRecordFolder"); + + // Sort out the child records + for (NodeRef record : rmService.getRecords(newNodeRef)) + { + removeUnwantedAspects(nodeService, record); + rmActionService.executeRecordsManagementAction(record, "file"); + } + } + + return null; + } + }, AuthenticationUtil.getAdminUserName()); + } + + /** + * Removes unwanted aspects + * + * @param nodeService + * @param nodeRef + */ + private void removeUnwantedAspects(NodeService nodeService, NodeRef nodeRef) + { + // Remove unwanted aspects + for (QName aspect : unwantedAspects) + { + if (nodeService.hasAspect(nodeRef, aspect) == true) + { + nodeService.removeAspect(nodeRef, aspect); + } + } + } + + /** + * Get the "do nothing" call back behaviour + * + * @param classRef + * @param copyDetails + * @return + */ + public CopyBehaviourCallback getDoNothingCopyCallback(QName classRef, CopyDetails copyDetails) + { + return new DoNothingCopyBehaviourCallback(); + } + + public CopyBehaviourCallback getIdCallback(QName classRef, CopyDetails copyDetails) + { + return new AbstractCopyBehaviourCallback() + { + public ChildAssocCopyAction getChildAssociationCopyAction( + QName classQName, + CopyDetails copyDetails, + CopyChildAssociationDetails childAssocCopyDetails) + { + return null; + } + + public Map getCopyProperties( + QName classQName, + CopyDetails copyDetails, + Map properties) + { + properties.put(PROP_IDENTIFIER, properties.get(PROP_IDENTIFIER) + "1"); + return properties; + } + + public boolean getMustCopy(QName classQName, CopyDetails copyDetails) + { + return true; + } + + }; + } + + /** + * Function to pad a string with zero '0' characters to the required length + * + * @param s String to pad with leading zero '0' characters + * @param len Length to pad to + * + * @return padded string or the original if already at >=len characters + */ + protected String padString(String s, int len) + { + String result = s; + for (int i=0; i<(len - s.length()); i++) + { + result = "0" + result; + } + return result; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordsManagementCustomModel.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordsManagementCustomModel.java new file mode 100644 index 0000000000..8751493a3a --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordsManagementCustomModel.java @@ -0,0 +1,52 @@ +/* + * 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.module.org_alfresco_module_rm.model; + +import org.alfresco.service.namespace.QName; + +/** + * Helper class containing records management custom model qualified names + * + * @author Gavin Cornwell + */ +public interface RecordsManagementCustomModel +{ + // Namespace details + public static String RM_CUSTOM_URI = "http://www.alfresco.org/model/rmcustom/1.0"; + public static String RM_CUSTOM_PREFIX = "rmc"; + + // Model + public static QName RM_CUSTOM_MODEL = QName.createQName(RM_CUSTOM_URI, "rmcustom"); + + // Custom constraint for Supplemental Marking List + public static QName CONSTRAINT_CUSTOM_SMLIST = QName.createQName(RM_CUSTOM_URI, "smList"); + + // Custom property for for Supplemental Marking List + public static QName PROP_SUPPLEMENTAL_MARKING_LIST = QName.createQName(RM_CUSTOM_URI, "supplementalMarkingList"); + + // Supplemental Marking List aspect + public static QName ASPECT_SUPPLEMENTAL_MARKING_LIST = QName.createQName(RM_CUSTOM_URI, "customSupplementalMarkingList"); + + // Custom associations aspect + public static QName ASPECT_CUSTOM_ASSOCIATIONS = QName.createQName(RM_CUSTOM_URI, "customAssocs"); + + // Some Custom references which are present on system startup. + public static QName CUSTOM_REF_VERSIONS = QName.createQName(RecordsManagementCustomModel.RM_CUSTOM_URI, "versions"); + public static QName CUSTOM_REF_SUPERSEDES = QName.createQName(RecordsManagementCustomModel.RM_CUSTOM_URI, "supersedes"); +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordsManagementModel.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordsManagementModel.java new file mode 100644 index 0000000000..2f523e76ab --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordsManagementModel.java @@ -0,0 +1,204 @@ +/* + * 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.module.org_alfresco_module_rm.model; + +import org.alfresco.service.namespace.QName; + +/** + * Helper class containing records management qualified names + * + * @author Roy Wetherall + */ +public interface RecordsManagementModel extends RecordsManagementCustomModel +{ + // Namespace details + public static final String RM_URI = "http://www.alfresco.org/model/recordsmanagement/1.0"; + public static final String RM_PREFIX = "rma"; + + // Model + public static final QName RM_MODEL = QName.createQName(RM_URI, "recordsmanagement"); + + // RM Site + public static final QName TYPE_RM_SITE = QName.createQName(RM_URI, "rmsite"); + + // Caveat config + public static final QName TYPE_CAVEAT_CONFIG = QName.createQName(RM_URI, "caveatConfig"); + + public static final QName ASPECT_CAVEAT_CONFIG_ROOT = QName.createQName(RM_URI, "caveatConfigRoot"); + public static final QName ASSOC_CAVEAT_CONFIG = QName.createQName(RM_URI, "caveatConfigAssoc"); + + // Email config + public static final QName TYPE_EMAIL_CONFIG = QName.createQName(RM_URI, "emailConfig"); + public static final QName ASPECT_EMAIL_CONFIG_ROOT = QName.createQName(RM_URI, "emailConfigRoot"); + public static final QName ASSOC_EMAIL_CONFIG = QName.createQName(RM_URI, "emailConfigAssoc"); + + // Records management container + public static final QName TYPE_RECORDS_MANAGEMENT_CONTAINER = QName.createQName(RM_URI, "recordsManagementContainer"); + + // Record Category + public static final QName TYPE_RECORD_CATEGORY = QName.createQName(RM_URI, "recordCategory"); + + // Records management root container + public static final QName TYPE_FILE_PLAN = QName.createQName(RM_URI, "filePlan"); + + // Disposition instructions aspect + public static final QName ASPECT_SCHEDULED = QName.createQName(RM_URI, "scheduled"); + public static final QName ASSOC_DISPOSITION_SCHEDULE = QName.createQName(RM_URI, "dispositionSchedule"); + + // Disposition definition type + public static final QName TYPE_DISPOSITION_SCHEDULE = QName.createQName(RM_URI, "dispositionSchedule"); + public static final QName PROP_DISPOSITION_AUTHORITY = QName.createQName(RM_URI, "dispositionAuthority"); + public static final QName PROP_DISPOSITION_INSTRUCTIONS = QName.createQName(RM_URI, "dispositionInstructions"); + public static final QName PROP_RECORD_LEVEL_DISPOSITION = QName.createQName(RM_URI, "recordLevelDisposition"); + public static final QName ASSOC_DISPOSITION_ACTION_DEFINITIONS = QName.createQName(RM_URI, "dispositionActionDefinitions"); + + // Disposition action type + public static final QName TYPE_DISPOSITION_ACTION_DEFINITION = QName.createQName(RM_URI, "dispositionActionDefinition"); + public static final QName PROP_DISPOSITION_ACTION_NAME = QName.createQName(RM_URI, "dispositionActionName"); + public static final QName PROP_DISPOSITION_DESCRIPTION = QName.createQName(RM_URI, "dispositionDescription"); + public static final QName PROP_DISPOSITION_PERIOD = QName.createQName(RM_URI, "dispositionPeriod"); + public static final QName PROP_DISPOSITION_PERIOD_PROPERTY = QName.createQName(RM_URI, "dispositionPeriodProperty"); + public static final QName PROP_DISPOSITION_EVENT = QName.createQName(RM_URI, "dispositionEvent"); + public static final QName PROP_DISPOSITION_EVENT_COMBINATION = QName.createQName(RM_URI, "dispositionEventCombination"); + public static final QName PROP_DISPOSITION_LOCATION = QName.createQName(RM_URI, "dispositionLocation"); + + // Records folder + public static final QName TYPE_RECORD_FOLDER = QName.createQName(RM_URI, "recordFolder"); + public static final QName PROP_IS_CLOSED = QName.createQName(RM_URI, "isClosed"); + + // Declared record aspect + public static final QName ASPECT_DECLARED_RECORD = QName.createQName(RM_URI, "declaredRecord"); + public static final QName PROP_DECLARED_AT = QName.createQName(RM_URI, "declaredAt"); + public static final QName PROP_DECLARED_BY = QName.createQName(RM_URI, "declaredBy"); + + // Record aspect + public static final QName ASPECT_RECORD = QName.createQName(RM_URI, "record"); + public static final QName PROP_DATE_FILED = QName.createQName(RM_URI, "dateFiled"); + public static final QName PROP_ORIGINATOR = QName.createQName(RM_URI, "originator"); + public static final QName PROP_ORIGINATING_ORGANIZATION = QName.createQName(RM_URI, "originatingOrganization"); + public static final QName PROP_PUBLICATION_DATE = QName.createQName(RM_URI, "publicationDate"); + public static final QName PROP_MEDIA_TYPE = QName.createQName(RM_URI, "mediaType"); + public static final QName PROP_FORMAT = QName.createQName(RM_URI, "format"); + public static final QName PROP_DATE_RECEIVED = QName.createQName(RM_URI, "dateReceived"); + + // Common record details + public static final QName PROP_LOCATION = QName.createQName(RM_URI, "location"); + + // Fileable aspect + public static final QName ASPECT_FILABLE = QName.createQName(RM_URI, "fileable"); + + // Record component identifier aspect + public static final QName ASPECT_RECORD_COMPONENT_ID = QName.createQName(RM_URI, "recordComponentIdentifier"); + public static final QName PROP_IDENTIFIER = QName.createQName(RM_URI, "identifier"); + public static final QName PROP_DB_UNIQUENESS_ID = QName.createQName(RM_URI, "dbUniquenessId"); + + // Vital record definition aspect + public static final QName ASPECT_VITAL_RECORD_DEFINITION = QName.createQName(RM_URI, "vitalRecordDefinition"); + public static final QName PROP_VITAL_RECORD_INDICATOR = QName.createQName(RM_URI, "vitalRecordIndicator"); + public static final QName PROP_REVIEW_PERIOD = QName.createQName(RM_URI, "reviewPeriod"); + + // Vital record aspect + public static final QName ASPECT_VITAL_RECORD = QName.createQName(RM_URI, "vitalRecord"); + public static final QName PROP_REVIEW_AS_OF = QName.createQName(RM_URI, "reviewAsOf"); + public static final QName PROP_NOTIFICATION_ISSUED = QName.createQName(RM_URI, "notificationIssued"); + + // Cut off aspect + public static final QName ASPECT_CUT_OFF = QName.createQName(RM_URI, "cutOff"); + public static final QName PROP_CUT_OFF_DATE = QName.createQName(RM_URI, "cutOffDate"); + + // Transferred aspect + public static final QName ASPECT_TRANSFERRED = QName.createQName(RM_URI, "transferred"); + + // Ascended aspect + public static final QName ASPECT_ASCENDED = QName.createQName(RM_URI, "ascended"); + + // Disposition schedule aspect + public static final QName ASPECT_DISPOSITION_LIFECYCLE = QName.createQName(RM_URI, "dispositionLifecycle"); + public static final QName ASSOC_NEXT_DISPOSITION_ACTION = QName.createQName(RM_URI, "nextDispositionAction"); + public static final QName ASSOC_DISPOSITION_ACTION_HISTORY = QName.createQName(RM_URI, "dispositionActionHistory"); + + // Disposition action type + public static final QName TYPE_DISPOSITION_ACTION = QName.createQName(RM_URI, "dispositionAction"); + public static final QName PROP_DISPOSITION_ACTION_ID = QName.createQName(RM_URI, "dispositionActionId"); + public static final QName PROP_DISPOSITION_ACTION = QName.createQName(RM_URI, "dispositionAction"); + public static final QName PROP_DISPOSITION_AS_OF = QName.createQName(RM_URI, "dispositionAsOf"); + public static final QName PROP_DISPOSITION_EVENTS_ELIGIBLE = QName.createQName(RM_URI, "dispositionEventsEligible"); + public static final QName PROP_DISPOSITION_ACTION_STARTED_AT = QName.createQName(RM_URI, "dispositionActionStartedAt"); + public static final QName PROP_DISPOSITION_ACTION_STARTED_BY = QName.createQName(RM_URI, "dispositionActionStartedBy"); + public static final QName PROP_DISPOSITION_ACTION_COMPLETED_AT = QName.createQName(RM_URI, "dispositionActionCompletedAt"); + public static final QName PROP_DISPOSITION_ACTION_COMPLETED_BY = QName.createQName(RM_URI, "dispositionActionCompletedBy"); + public static final QName ASSOC_EVENT_EXECUTIONS = QName.createQName(RM_URI, "eventExecutions"); + + // Event execution type + public static final QName TYPE_EVENT_EXECUTION = QName.createQName(RM_URI, "eventExecution"); + public static final QName PROP_EVENT_EXECUTION_NAME = QName.createQName(RM_URI, "eventExecutionName"); + public static final QName PROP_EVENT_EXECUTION_AUTOMATIC = QName.createQName(RM_URI, "eventExecutionAutomatic"); + public static final QName PROP_EVENT_EXECUTION_COMPLETE = QName.createQName(RM_URI, "eventExecutionComplete"); + public static final QName PROP_EVENT_EXECUTION_COMPLETED_BY = QName.createQName(RM_URI, "eventExecutionCompletedBy"); + public static final QName PROP_EVENT_EXECUTION_COMPLETED_AT = QName.createQName(RM_URI, "eventExecutionCompletedAt"); + + // Custom RM data aspect + public static final QName ASPECT_CUSTOM_RM_DATA = QName.createQName(RM_URI, "customRMData"); + + // marker aspect on all RM objercts (except caveat root) + public static final QName ASPECT_FILE_PLAN_COMPONENT = QName.createQName(RM_URI, "filePlanComponent"); + public static final QName PROP_ROOT_NODEREF = QName.createQName(RM_URI, "rootNodeRef"); + + // Non-electronic document + public static final QName TYPE_NON_ELECTRONIC_DOCUMENT = QName.createQName(RM_URI, "nonElectronicDocument"); + + // Records management root aspect + public static final QName ASPECT_RECORDS_MANAGEMENT_ROOT = QName.createQName(RM_URI, "recordsManagementRoot"); + public static final QName ASSOC_HOLDS = QName.createQName(RM_URI, "holds"); + public static final QName ASSOC_TRANSFERS = QName.createQName(RM_URI, "transfers"); + + // Hold type + public static final QName TYPE_HOLD = QName.createQName(RM_URI, "hold"); + public static final QName PROP_HOLD_REASON = QName.createQName(RM_URI, "holdReason"); + public static final QName ASSOC_FROZEN_RECORDS = QName.createQName(RM_URI, "frozenRecords"); + + // Record meta data aspect + public static final QName ASPECT_RECORD_META_DATA = QName.createQName(RM_URI, "recordMetaData"); + + // Frozen aspect + public static final QName ASPECT_FROZEN = QName.createQName(RM_URI, "frozen"); + public static final QName PROP_FROZEN_AT = QName.createQName(RM_URI, "frozenAt"); + public static final QName PROP_FROZEN_BY = QName.createQName(RM_URI, "frozenBy"); + + // Transfer aspect + public static final QName TYPE_TRANSFER = QName.createQName(RM_URI, "transfer"); + public static final QName PROP_TRANSFER_ACCESSION_INDICATOR = QName.createQName(RM_URI, "transferAccessionIndicator"); + public static final QName PROP_TRANSFER_PDF_INDICATOR = QName.createQName(RM_URI, "transferPDFIndicator"); + public static final QName PROP_TRANSFER_LOCATION = QName.createQName(RM_URI, "transferLocation"); + public static final QName ASSOC_TRANSFERRED = QName.createQName(RM_URI, "transferred"); + + // Versioned record aspect + public static final QName ASPECT_VERSIONED_RECORD = QName.createQName(RM_URI, "versionedRecord"); + + // Unpublished update aspect + public static final QName ASPECT_UNPUBLISHED_UPDATE = QName.createQName(RM_URI, "unpublishedUpdate"); + public static final QName PROP_UNPUBLISHED_UPDATE = QName.createQName(RM_URI, "unpublishedUpdate"); + public static final QName PROP_UPDATE_TO = QName.createQName(RM_URI, "updateTo"); + public static final QName PROP_UPDATED_PROPERTIES = QName.createQName(RM_URI, "updatedProperties"); + public static final QName PROP_PUBLISH_IN_PROGRESS = QName.createQName(RM_URI, "publishInProgress"); + public static final String UPDATE_TO_DISPOSITION_ACTION_DEFINITION = "dispositionActionDefinition"; + + // Ghosted aspect + public static QName ASPECT_GHOSTED = QName.createQName(RM_URI, "ghosted"); +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordsManagementSearchBehaviour.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordsManagementSearchBehaviour.java new file mode 100644 index 0000000000..c7f33df3a3 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordsManagementSearchBehaviour.java @@ -0,0 +1,699 @@ +/* + * 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.module.org_alfresco_module_rm.model; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementServiceRegistry; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionAction; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionActionDefinition; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionActionImpl; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionScheduleImpl; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService; +import org.alfresco.module.org_alfresco_module_rm.event.EventCompletionDetails; +import org.alfresco.module.org_alfresco_module_rm.vital.VitalRecordDefinition; +import org.alfresco.module.org_alfresco_module_rm.vital.VitalRecordService; +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.policy.Behaviour.NotificationFrequency; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +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.Period; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Search Behaviour class. + * + * Manages the collapse of data onto the supporting aspect on the record/record folder + * + * @author Roy Wetherall + */ +public class RecordsManagementSearchBehaviour implements RecordsManagementModel +{ + private static Log logger = LogFactory.getLog(RecordsManagementSearchBehaviour.class); + + /** Search specific elements of the RM model */ + public static final QName ASPECT_RM_SEARCH = QName.createQName(RM_URI, "recordSearch"); + public static final QName PROP_RS_DISPOSITION_ACTION_NAME = QName.createQName(RM_URI, "recordSearchDispositionActionName"); + public static final QName PROP_RS_DISPOSITION_ACTION_AS_OF = QName.createQName(RM_URI, "recordSearchDispositionActionAsOf"); + public static final QName PROP_RS_DISPOSITION_EVENTS_ELIGIBLE = QName.createQName(RM_URI, "recordSearchDispositionEventsEligible"); + public static final QName PROP_RS_DISPOSITION_EVENTS = QName.createQName(RM_URI, "recordSearchDispositionEvents"); + public static final QName PROP_RS_VITAL_RECORD_REVIEW_PERIOD = QName.createQName(RM_URI, "recordSearchVitalRecordReviewPeriod"); + public static final QName PROP_RS_VITAL_RECORD_REVIEW_PERIOD_EXPRESSION = QName.createQName(RM_URI, "recordSearchVitalRecordReviewPeriodExpression"); + public static final QName PROP_RS_DISPOSITION_PERIOD = QName.createQName(RM_URI, "recordSearchDispositionPeriod"); + public static final QName PROP_RS_DISPOSITION_PERIOD_EXPRESSION = QName.createQName(RM_URI, "recordSearchDispositionPeriodExpression"); + public static final QName PROP_RS_HAS_DISPOITION_SCHEDULE = QName.createQName(RM_URI, "recordSearchHasDispositionSchedule"); + public static final QName PROP_RS_DISPOITION_INSTRUCTIONS = QName.createQName(RM_URI, "recordSearchDispositionInstructions"); + public static final QName PROP_RS_DISPOITION_AUTHORITY = QName.createQName(RM_URI, "recordSearchDispositionAuthority"); + public static final QName PROP_RS_HOLD_REASON = QName.createQName(RM_URI, "recordSearchHoldReason"); + + /** Policy component */ + private PolicyComponent policyComponent; + + /** Node service */ + private NodeService nodeService; + + /** Records management service */ + private RecordsManagementService recordsManagementService; + + /** Disposition service */ + private DispositionService dispositionService; + + /** Records management service registry */ + private RecordsManagementServiceRegistry recordsManagementServiceRegistry; + + /** Vital record service */ + private VitalRecordService vitalRecordService; + + /** + * @param nodeService the nodeService to set + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * @param dispositionService the disposition service + */ + public void setDispositionService(DispositionService dispositionService) + { + this.dispositionService = dispositionService; + } + + /** + * @param policyComponent the policyComponent to set + */ + public void setPolicyComponent(PolicyComponent policyComponent) + { + this.policyComponent = policyComponent; + } + + /** + * @param recordsManagementService the records management service + */ + public void setRecordsManagementService(RecordsManagementService recordsManagementService) + { + this.recordsManagementService = recordsManagementService; + } + + /** + * @param recordsManagementServiceRegistry the records management service registry + */ + public void setRecordsManagementServiceRegistry(RecordsManagementServiceRegistry recordsManagementServiceRegistry) + { + this.recordsManagementServiceRegistry = recordsManagementServiceRegistry; + } + + /** + * @param vitalRecordService vital record service + */ + public void setVitalRecordService(VitalRecordService vitalRecordService) + { + this.vitalRecordService = vitalRecordService; + } + + /** Java behaviour */ + private JavaBehaviour onAddSearchAspect = new JavaBehaviour(this, "rmSearchAspectAdd", NotificationFrequency.TRANSACTION_COMMIT); + + /** + * Initialisation method + */ + public void init() + { + this.policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onCreateNode"), + TYPE_DISPOSITION_ACTION, + new JavaBehaviour(this, "dispositionActionCreate", NotificationFrequency.TRANSACTION_COMMIT)); + + this.policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onUpdateProperties"), + TYPE_DISPOSITION_ACTION, + new JavaBehaviour(this, "dispositionActionPropertiesUpdate", NotificationFrequency.TRANSACTION_COMMIT)); + + this.policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onUpdateProperties"), + TYPE_DISPOSITION_SCHEDULE, + new JavaBehaviour(this, "dispositionSchedulePropertiesUpdate", NotificationFrequency.TRANSACTION_COMMIT)); + + this.policyComponent.bindAssociationBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onCreateChildAssociation"), + TYPE_DISPOSITION_ACTION, + ASSOC_EVENT_EXECUTIONS, + new JavaBehaviour(this, "eventExecutionUpdate", NotificationFrequency.TRANSACTION_COMMIT)); + + this.policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onDeleteNode"), + TYPE_EVENT_EXECUTION, + new JavaBehaviour(this, "eventExecutionDelete", NotificationFrequency.TRANSACTION_COMMIT)); + + this.policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onAddAspect"), + ASPECT_RM_SEARCH, + onAddSearchAspect); + + this.policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onAddAspect"), + ASPECT_RECORD, + new JavaBehaviour(this, "onAddRecordAspect", NotificationFrequency.TRANSACTION_COMMIT)); + + this.policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onCreateNode"), + TYPE_RECORD_FOLDER, + new JavaBehaviour(this, "recordFolderCreate", NotificationFrequency.TRANSACTION_COMMIT)); + + // Vital Records Review Details Rollup + this.policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onAddAspect"), + ASPECT_VITAL_RECORD_DEFINITION, + new JavaBehaviour(this, "vitalRecordDefintionAddAspect", NotificationFrequency.TRANSACTION_COMMIT)); + this.policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onUpdateProperties"), + ASPECT_VITAL_RECORD_DEFINITION, + new JavaBehaviour(this, "vitalRecordDefintionUpdateProperties", NotificationFrequency.TRANSACTION_COMMIT)); + + // Hold reason rollup + this.policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onRemoveAspect"), + ASPECT_FROZEN, + new JavaBehaviour(this, "onRemoveFrozenAspect", NotificationFrequency.TRANSACTION_COMMIT)); + + this.policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onUpdateProperties"), + TYPE_HOLD, + new JavaBehaviour(this, "frozenAspectUpdateProperties", NotificationFrequency.TRANSACTION_COMMIT)); + } + + /** + * Ensures the search aspect for the given node is present, complete and correct. + * + * @param recordOrFolder + */ + public void fixupSearchAspect(NodeRef recordOrFolder) + { + // for now only deal with record folders + if (recordsManagementService.isRecordFolder(recordOrFolder)) + { + // ensure the search aspect is applied + applySearchAspect(recordOrFolder); + + // setup the properties relating to the disposition schedule + setupDispositionScheduleProperties(recordOrFolder); + + // setup the properties relating to the disposition lifecycle + DispositionAction da = dispositionService.getNextDispositionAction(recordOrFolder); + if (da != null) + { + updateDispositionActionProperties(recordOrFolder, da.getNodeRef()); + setupDispositionActionEvents(recordOrFolder, da); + } + + // setup the properties relating to the vital record indicator + setVitalRecordDefintionDetails(recordOrFolder); + } + } + + /** + * Updates the disposition action properties + * + * @param nodeRef + * @param before + * @param after + */ + public void dispositionActionPropertiesUpdate(final NodeRef nodeRef, final Map before, final Map after) + { + if (this.nodeService.exists(nodeRef) == true) + { + AuthenticationUtil.runAs(new RunAsWork() + { + @Override + public Void doWork() throws Exception + { + ChildAssociationRef assoc = nodeService.getPrimaryParent(nodeRef); + if (assoc.getTypeQName().equals(ASSOC_NEXT_DISPOSITION_ACTION) == true) + { + // Get the record (or record folder) + NodeRef record = assoc.getParentRef(); + + // Apply the search aspect + applySearchAspect(record); + + // Update disposition properties + updateDispositionActionProperties(record, nodeRef); + } + + return null; + + }}, AuthenticationUtil.getSystemUserName()); + } + } + + private void applySearchAspect(NodeRef nodeRef) + { + onAddSearchAspect.disable(); + try + { + if (this.nodeService.hasAspect(nodeRef, ASPECT_RM_SEARCH) == false) + { + this.nodeService.addAspect(nodeRef, ASPECT_RM_SEARCH , null); + + if (logger.isDebugEnabled()) + logger.debug("Added search aspect to node: " + nodeRef); + } + } + finally + { + onAddSearchAspect.enable(); + } + } + + public void onAddRecordAspect(NodeRef nodeRef, QName aspectTypeQName) + { + if (nodeService.exists(nodeRef) == true) + { + applySearchAspect(nodeRef); + setupDispositionScheduleProperties(nodeRef); + } + } + + public void recordFolderCreate(ChildAssociationRef childAssocRef) + { + NodeRef nodeRef = childAssocRef.getChildRef(); + if (nodeService.exists(nodeRef) == true) + { + applySearchAspect(nodeRef); + setupDispositionScheduleProperties(nodeRef); + } + } + + private void setupDispositionScheduleProperties(NodeRef recordOrFolder) + { + DispositionSchedule ds = dispositionService.getDispositionSchedule(recordOrFolder); + if (ds == null) + { + nodeService.setProperty(recordOrFolder, PROP_RS_HAS_DISPOITION_SCHEDULE, false); + } + else + { + nodeService.setProperty(recordOrFolder, PROP_RS_HAS_DISPOITION_SCHEDULE, true); + setDispositionScheduleProperties(recordOrFolder, ds); + } + + if (logger.isDebugEnabled()) + { + logger.debug("Set rma:recordSearchHasDispositionSchedule for node " + recordOrFolder + + " to: " + (ds != null)); + } + } + + public void dispositionActionCreate(ChildAssociationRef childAssocRef) + { + NodeRef child = childAssocRef.getChildRef(); + if (nodeService.exists(child) == true && + childAssocRef.getTypeQName().equals(ASSOC_NEXT_DISPOSITION_ACTION) == true) + { + // Get the record (or record folder) + NodeRef record = childAssocRef.getParentRef(); + + // Apply the search aspect + applySearchAspect(record); + + // Update disposition properties + updateDispositionActionProperties(record, childAssocRef.getChildRef()); + + // Clear the events + this.nodeService.setProperty(record, PROP_RS_DISPOSITION_EVENTS, null); + } + } + + /** + * + * @param record + * @param dispositionAction + */ + private void updateDispositionActionProperties(NodeRef record, NodeRef dispositionAction) + { + Map props = nodeService.getProperties(record); + + DispositionAction da = new DispositionActionImpl(recordsManagementServiceRegistry, dispositionAction); + + props.put(PROP_RS_DISPOSITION_ACTION_NAME, da.getName()); + props.put(PROP_RS_DISPOSITION_ACTION_AS_OF, da.getAsOfDate()); + props.put(PROP_RS_DISPOSITION_EVENTS_ELIGIBLE, this.nodeService.getProperty(dispositionAction, PROP_DISPOSITION_EVENTS_ELIGIBLE)); + + DispositionActionDefinition daDefinition = da.getDispositionActionDefinition(); + Period period = daDefinition.getPeriod(); + if (period != null) + { + props.put(PROP_RS_DISPOSITION_PERIOD, period.getPeriodType()); + props.put(PROP_RS_DISPOSITION_PERIOD_EXPRESSION, period.getExpression()); + } + else + { + props.put(PROP_RS_DISPOSITION_PERIOD, null); + props.put(PROP_RS_DISPOSITION_PERIOD_EXPRESSION, null); + } + + nodeService.setProperties(record, props); + + if (logger.isDebugEnabled()) + { + logger.debug("Set rma:recordSearchDispositionActionName for node " + record + " to: " + + props.get(PROP_RS_DISPOSITION_ACTION_NAME)); + logger.debug("Set rma:recordSearchDispositionActionAsOf for node " + record + " to: " + + props.get(PROP_RS_DISPOSITION_ACTION_AS_OF)); + logger.debug("Set rma:recordSearchDispositionEventsEligible for node " + record + " to: " + + props.get(PROP_RS_DISPOSITION_EVENTS_ELIGIBLE)); + logger.debug("Set rma:recordSearchDispositionPeriod for node " + record + " to: " + + props.get(PROP_RS_DISPOSITION_PERIOD)); + logger.debug("Set rma:recordSearchDispositionPeriodExpression for node " + record + " to: " + + props.get(PROP_RS_DISPOSITION_PERIOD_EXPRESSION)); + } + } + + @SuppressWarnings("unchecked") + public void eventExecutionUpdate(ChildAssociationRef childAssocRef, boolean isNewNode) + { + NodeRef dispositionAction = childAssocRef.getParentRef(); + NodeRef eventExecution = childAssocRef.getChildRef(); + + if (this.nodeService.exists(dispositionAction) == true && + this.nodeService.exists(eventExecution) == true) + { + ChildAssociationRef assoc = this.nodeService.getPrimaryParent(dispositionAction); + if (assoc.getTypeQName().equals(ASSOC_NEXT_DISPOSITION_ACTION) == true) + { + // Get the record (or record folder) + NodeRef record = assoc.getParentRef(); + + // Apply the search aspect + applySearchAspect(record); + + Collection events = (List)this.nodeService.getProperty(record, PROP_RS_DISPOSITION_EVENTS); + if (events == null) + { + events = new ArrayList(1); + } + events.add((String)this.nodeService.getProperty(eventExecution, PROP_EVENT_EXECUTION_NAME)); + this.nodeService.setProperty(record, PROP_RS_DISPOSITION_EVENTS, (Serializable)events); + } + } + } + + public void eventExecutionDelete(ChildAssociationRef childAssocRef, boolean isNodeArchived) + { + NodeRef dispositionActionNode = childAssocRef.getParentRef(); + + if (this.nodeService.exists(dispositionActionNode)) + { + ChildAssociationRef assoc = this.nodeService.getPrimaryParent(dispositionActionNode); + if (assoc.getTypeQName().equals(ASSOC_NEXT_DISPOSITION_ACTION) == true) + { + // Get the record (or record folder) + NodeRef record = assoc.getParentRef(); + + // Apply the search aspect + applySearchAspect(record); + + // make sure the list of events match the action definition + setupDispositionActionEvents(record, dispositionService.getNextDispositionAction(record)); + } + } + } + + private void setupDispositionActionEvents(NodeRef nodeRef, DispositionAction da) + { + if (da != null) + { + List eventNames = null; + List eventsList = da.getEventCompletionDetails(); + if (eventsList.size() > 0) + { + eventNames = new ArrayList(eventsList.size()); + for (EventCompletionDetails event : eventsList) + { + eventNames.add(event.getEventName()); + } + } + + // set the property + this.nodeService.setProperty(nodeRef, PROP_RS_DISPOSITION_EVENTS, (Serializable)eventNames); + + if (logger.isDebugEnabled()) + { + logger.debug("Set rma:recordSearchDispositionEvents for node " + nodeRef + " to: " + eventNames); + } + } + } + + public void rmSearchAspectAdd(NodeRef nodeRef, QName aspectTypeQName) + { + if (nodeService.exists(nodeRef) == true) + { + // Initialise the search parameteres as required + setVitalRecordDefintionDetails(nodeRef); + } + } + + public void vitalRecordDefintionAddAspect(NodeRef nodeRef, QName aspectTypeQName) + { + // Only care about record folders + if (recordsManagementService.isRecordFolder(nodeRef) == true) + { + updateVitalRecordDefinitionValues(nodeRef); + } + } + + public void vitalRecordDefintionUpdateProperties(NodeRef nodeRef, Map before, Map after) + { + // Only care about record folders + if (recordsManagementService.isRecordFolder(nodeRef) == true) + { + Set props = new HashSet(1); + props.add(PROP_REVIEW_PERIOD); + Set changed = determineChangedProps(before, after); + changed.retainAll(props); + if (changed.isEmpty() == false) + { + updateVitalRecordDefinitionValues(nodeRef); + } + + } + } + + private void updateVitalRecordDefinitionValues(NodeRef nodeRef) + { + // ensure the folder itself reflects the correct details + applySearchAspect(nodeRef); + setVitalRecordDefintionDetails(nodeRef); + + List records = recordsManagementService.getRecords(nodeRef); + for (NodeRef record : records) + { + // Apply the search aspect + applySearchAspect(record); + + // Set the vital record definition details + setVitalRecordDefintionDetails(record); + } + } + + private void setVitalRecordDefintionDetails(NodeRef nodeRef) + { + VitalRecordDefinition vrd = vitalRecordService.getVitalRecordDefinition(nodeRef); + + if (vrd != null && vrd.isEnabled() == true && vrd.getReviewPeriod() != null) + { + // Set the property values + nodeService.setProperty(nodeRef, PROP_RS_VITAL_RECORD_REVIEW_PERIOD, vrd.getReviewPeriod().getPeriodType()); + nodeService.setProperty(nodeRef, PROP_RS_VITAL_RECORD_REVIEW_PERIOD_EXPRESSION, vrd.getReviewPeriod().getExpression()); + + if (logger.isDebugEnabled()) + { + logger.debug("Set rma:recordSearchVitalRecordReviewPeriod for node " + nodeRef + " to: " + + vrd.getReviewPeriod().getPeriodType()); + logger.debug("Set rma:recordSearchVitalRecordReviewPeriodExpression for node " + nodeRef + " to: " + + vrd.getReviewPeriod().getExpression()); + } + } + else + { + // Clear the vital record properties + nodeService.setProperty(nodeRef, PROP_RS_VITAL_RECORD_REVIEW_PERIOD, null); + nodeService.setProperty(nodeRef, PROP_RS_VITAL_RECORD_REVIEW_PERIOD_EXPRESSION, null); + } + } + + public void onRemoveFrozenAspect(NodeRef nodeRef, QName aspectTypeQName) + { + if (nodeService.exists(nodeRef) == true && + nodeService.hasAspect(nodeRef, ASPECT_RM_SEARCH)) + { + nodeService.setProperty(nodeRef, PROP_RS_HOLD_REASON, null); + } + } + + public void frozenAspectUpdateProperties(final NodeRef nodeRef, final Map before, final Map after) + { + AuthenticationUtil.RunAsWork work = new AuthenticationUtil.RunAsWork() + { + @Override + public Void doWork() throws Exception + { + if (nodeService.exists(nodeRef) == true) + { + // get the changed hold reason + String holdReason = (String)nodeService.getProperty(nodeRef, PROP_HOLD_REASON); + + // get all the frozen items the hold node has and change the hold reason + List holdAssocs = nodeService.getChildAssocs(nodeRef, ASSOC_FROZEN_RECORDS, RegexQNamePattern.MATCH_ALL); + for (ChildAssociationRef assoc : holdAssocs) + { + NodeRef frozenItem = assoc.getChildRef(); + + // ensure the search aspect is applied and set the hold reason + applySearchAspect(frozenItem); + nodeService.setProperty(frozenItem, PROP_RS_HOLD_REASON, holdReason); + } + } + + return null; + } + }; + + AuthenticationUtil.runAs(work, AuthenticationUtil.getSystemUserName()); + } + + /** + * Updates the disposition schedule properties + * + * @param nodeRef + * @param before + * @param after + */ + public void dispositionSchedulePropertiesUpdate(NodeRef nodeRef, Map before, Map after) + { + if (this.nodeService.exists(nodeRef) == true) + { + // create the schedule object and get the record category for it + DispositionSchedule schedule = new DispositionScheduleImpl(this.recordsManagementServiceRegistry, this.nodeService, nodeRef); + NodeRef recordCategoryNode = this.nodeService.getPrimaryParent(schedule.getNodeRef()).getParentRef(); + + if (schedule.isRecordLevelDisposition()) + { + for (NodeRef recordFolder : this.getRecordFolders(recordCategoryNode)) + { + for (NodeRef record : this.recordsManagementService.getRecords(recordFolder)) + { + applySearchAspect(record); + setDispositionScheduleProperties(record, schedule); + } + } + } + else + { + for (NodeRef recordFolder : this.getRecordFolders(recordCategoryNode)) + { + applySearchAspect(recordFolder); + setDispositionScheduleProperties(recordFolder, schedule); + } + } + } + } + + private void setDispositionScheduleProperties(NodeRef recordOrFolder, DispositionSchedule schedule) + { + if (schedule != null) + { + this.nodeService.setProperty(recordOrFolder, PROP_RS_DISPOITION_AUTHORITY, schedule.getDispositionAuthority()); + this.nodeService.setProperty(recordOrFolder, PROP_RS_DISPOITION_INSTRUCTIONS, schedule.getDispositionInstructions()); + + if (logger.isDebugEnabled()) + { + logger.debug("Set rma:recordSearchDispositionAuthority for node " + recordOrFolder + " to: " + schedule.getDispositionAuthority()); + logger.debug("Set rma:recordSearchDispositionInstructions for node " + recordOrFolder + " to: " + schedule.getDispositionInstructions()); + } + } + } + + /** + * This method compares the oldProps map against the newProps map and returns + * a set of QNames of the properties that have changed. Changed here means one of + *
    + *
  • the property has been removed
  • + *
  • the property has had its value changed
  • + *
  • the property has been added
  • + *
+ */ + private Set determineChangedProps(Map oldProps, Map newProps) + { + Set result = new HashSet(); + for (QName qn : oldProps.keySet()) + { + if (newProps.get(qn) == null || + newProps.get(qn).equals(oldProps.get(qn)) == false) + { + result.add(qn); + } + } + for (QName qn : newProps.keySet()) + { + if (oldProps.get(qn) == null) + { + result.add(qn); + } + } + + return result; + } + + private List getRecordFolders(NodeRef recordCategoryNode) + { + List results = new ArrayList(8); + + List folderAssocs = nodeService.getChildAssocs(recordCategoryNode, + ContentModel.ASSOC_CONTAINS, RegexQNamePattern.MATCH_ALL); + for (ChildAssociationRef folderAssoc : folderAssocs) + { + NodeRef folder = folderAssoc.getChildRef(); + if (this.recordsManagementService.isRecordFolder(folder)) + { + results.add(folder); + } + } + + return results; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/RmSiteType.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/RmSiteType.java new file mode 100644 index 0000000000..8224640d20 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/RmSiteType.java @@ -0,0 +1,139 @@ +/* + * 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.module.org_alfresco_module_rm.model; + +import org.alfresco.module.org_alfresco_module_rm.search.RecordsManagementSearchService; +import org.alfresco.repo.node.NodeServicePolicies; +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.policy.Behaviour.NotificationFrequency; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +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.cmr.site.SiteInfo; +import org.alfresco.service.cmr.site.SiteService; + +/** + * Behaviour associated with the RM Site type + * + * @author Roy Wetherall + */ +public class RmSiteType implements RecordsManagementModel, + NodeServicePolicies.OnCreateNodePolicy +{ + /** Constant values */ + public static final String COMPONENT_DOCUMENT_LIBRARY = "documentLibrary"; + public static final String DEFAULT_SITE_NAME = "rm"; + + /** Policy component */ + private PolicyComponent policyComponent; + + /** Site service */ + private SiteService siteService; + + /** Node service */ + private NodeService nodeService; + + /** Record Management Search Service */ + private RecordsManagementSearchService recordsManagementSearchService; + + /** + * Set the policy component + * @param policyComponent policy component + */ + public void setPolicyComponent(PolicyComponent policyComponent) + { + this.policyComponent = policyComponent; + } + + /** + * Set the site service + * @param siteService site service + */ + public void setSiteService(SiteService siteService) + { + this.siteService = siteService; + } + + /** + * Set node service + * @param nodeService node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * @param recordsManagementSearchService records management search service + */ + public void setRecordsManagementSearchService(RecordsManagementSearchService recordsManagementSearchService) + { + this.recordsManagementSearchService = recordsManagementSearchService; + } + + /** + * Bean initialisation method + */ + public void init() + { + policyComponent.bindClassBehaviour( + NodeServicePolicies.OnCreateNodePolicy.QNAME, + TYPE_RM_SITE, + new JavaBehaviour(this, "onCreateNode", NotificationFrequency.FIRST_EVENT)); + } + + /** + * @see org.alfresco.repo.node.NodeServicePolicies.OnCreateNodePolicy#onCreateNode(org.alfresco.service.cmr.repository.ChildAssociationRef) + */ + @Override + public void onCreateNode(ChildAssociationRef childAssocRef) + { + final NodeRef rmSite = childAssocRef.getChildRef(); + + // Do not execute behaviour if this has been created in the archive store + if(rmSite.getStoreRef().equals(StoreRef.STORE_REF_ARCHIVE_SPACESSTORE) == true) + { + // This is not the spaces store - probably the archive store + return; + } + + if (nodeService.exists(rmSite) == true) + { + AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public Object doWork() + { + SiteInfo siteInfo = siteService.getSite(rmSite); + if (siteInfo != null) + { + // Create the file plan component + siteService.createContainer(siteInfo.getShortName(), COMPONENT_DOCUMENT_LIBRARY, TYPE_FILE_PLAN, null); + + // Add the reports + recordsManagementSearchService.addReports(siteInfo.getShortName()); + } + return null; + } + }, AuthenticationUtil.getAdminUserName()); + } + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/ScheduledAspect.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/ScheduledAspect.java new file mode 100644 index 0000000000..22169f0417 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/ScheduledAspect.java @@ -0,0 +1,92 @@ +/* + * 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.module.org_alfresco_module_rm.model; + +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService; +import org.alfresco.repo.node.NodeServicePolicies; +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.policy.Behaviour.NotificationFrequency; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.QName; + +/** + * Behaviour associated with the scheduled aspect + * + * @author Roy Wetherall + */ +public class ScheduledAspect implements RecordsManagementModel, + NodeServicePolicies.OnAddAspectPolicy +{ + /** Policy component */ + private PolicyComponent policyComponent; + + private DispositionService dispositionService; + + /** Node service */ + private NodeService nodeService; + + /** + * Set the policy component + * @param policyComponent policy component + */ + public void setPolicyComponent(PolicyComponent policyComponent) + { + this.policyComponent = policyComponent; + } + + public void setDispositionService(DispositionService dispositionService) + { + this.dispositionService = dispositionService; + } + + /** + * Set node service + * @param nodeService node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Bean initialisation method + */ + public void init() + { + policyComponent.bindClassBehaviour( + NodeServicePolicies.OnAddAspectPolicy.QNAME, + ASPECT_SCHEDULED, + new JavaBehaviour(this, "onAddAspect", NotificationFrequency.TRANSACTION_COMMIT)); + } + + /** + * @see org.alfresco.repo.node.NodeServicePolicies.OnAddAspectPolicy#onAddAspect(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName) + */ + @Override + public void onAddAspect(NodeRef nodeRef, QName aspectTypeQName) + { + if (nodeService.exists(nodeRef) == true && + dispositionService.getAssociatedDispositionSchedule(nodeRef) == null) + { + dispositionService.createDispositionSchedule(nodeRef, null); + } + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/notification/RecordsManagementNotificationHelper.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/notification/RecordsManagementNotificationHelper.java new file mode 100644 index 0000000000..73e42eb40e --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/notification/RecordsManagementNotificationHelper.java @@ -0,0 +1,293 @@ +/* + * 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.module.org_alfresco_module_rm.notification; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService; +import org.alfresco.module.org_alfresco_module_rm.security.Role; +import org.alfresco.repo.model.Repository; +import org.alfresco.repo.notification.EMailNotificationProvider; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.service.cmr.notification.NotificationContext; +import org.alfresco.service.cmr.notification.NotificationService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.cmr.site.SiteInfo; +import org.alfresco.service.cmr.site.SiteService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.util.ParameterCheck; +import org.springframework.extensions.surf.util.I18NUtil; + +/** + * Helper bean containing methods useful when sending records + * management notifications via the {@link NotificationService} + * + * @author Roy Wetherall + */ +public class RecordsManagementNotificationHelper +{ + /** I18n */ + private static final String MSG_SUBJECT_RECORDS_DUE_FOR_REVIEW = "notification.dueforreview.subject"; + private static final String MSG_SUBJECT_RECORD_SUPERCEDED = "notification.superseded.subject"; + + /** Defaults */ + private static final String DEFAULT_SITE = "rm"; + + /** Services */ + private NotificationService notificationService; + private RecordsManagementService recordsManagementService; + private RecordsManagementSecurityService securityService; + private Repository repositoryHelper; + private SearchService searchService; + private NamespaceService namespaceService; + private SiteService siteService; + + /** Notification role */ + private String notificationRole; + + /** EMail notification templates */ + private NodeRef supersededTemplate = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, "record_superseded_template"); + private NodeRef dueForReviewTemplate; + + /** + * @param notificationService notification service + */ + public void setNotificationService(NotificationService notificationService) + { + this.notificationService = notificationService; + } + + /** + * @param recordsManagementService rm service + */ + public void setRecordsManagementService(RecordsManagementService recordsManagementService) + { + this.recordsManagementService = recordsManagementService; + } + + /** + * @param securityService rm security service + */ + public void setSecurityService(RecordsManagementSecurityService securityService) + { + this.securityService = securityService; + } + + /** + * @param notificationRole rm notification role + */ + public void setNotificationRole(String notificationRole) + { + this.notificationRole = notificationRole; + } + + /** + * + * @param repositoryHelper repository helper + */ + public void setRepositoryHelper(Repository repositoryHelper) + { + this.repositoryHelper = repositoryHelper; + } + + /** + * @param searchService search service + */ + public void setSearchService(SearchService searchService) + { + this.searchService = searchService; + } + + /** + * @param namespaceService namespace service + */ + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + /** + * @param siteService site service + */ + public void setSiteService(SiteService siteService) + { + this.siteService = siteService; + } + + /** + * @return superseded email template + */ + public NodeRef getSupersededTemplate() + { + return supersededTemplate; + } + + /** + * @return due for review email template + */ + public NodeRef getDueForReviewTemplate() + { + if (dueForReviewTemplate == null) + { + List nodeRefs = + searchService.selectNodes( + repositoryHelper.getRootHome(), + "app:company_home/app:dictionary/cm:records_management/cm:records_management_email_templates/cm:notify-records-due-for-review-email.ftl", null, + namespaceService, + false); + if (nodeRefs.size() == 1) + { + dueForReviewTemplate = nodeRefs.get(0); + } + } + return dueForReviewTemplate; + } + + /** + * Sends records due for review email notification. + * + * @param records records due for review + */ + public void recordsDueForReviewEmailNotification(final List records) + { + ParameterCheck.mandatory("records", records); + if (records.isEmpty() == false) + { + NodeRef root = getRMRoot(records.get(0)); + + NotificationContext notificationContext = new NotificationContext(); + notificationContext.setSubject(I18NUtil.getMessage(MSG_SUBJECT_RECORDS_DUE_FOR_REVIEW)); + notificationContext.setAsyncNotification(false); + notificationContext.setIgnoreNotificationFailure(true); + + notificationContext.setBodyTemplate(getDueForReviewTemplate()); + Map args = new HashMap(1, 1.0f); + args.put("records", (Serializable)records); + args.put("site", getSiteName(root)); + notificationContext.setTemplateArgs(args); + + String groupName = getGroupName(root); + notificationContext.addTo(groupName); + + notificationService.sendNotification(EMailNotificationProvider.NAME, notificationContext); + } + } + + /** + * Sends record superseded email notification. + * + * @param record superseded record + */ + public void recordSupersededEmailNotification(final NodeRef record) + { + ParameterCheck.mandatory("record", record); + + NodeRef root = getRMRoot(record); + + NotificationContext notificationContext = new NotificationContext(); + notificationContext.setSubject(I18NUtil.getMessage(MSG_SUBJECT_RECORD_SUPERCEDED)); + notificationContext.setAsyncNotification(false); + notificationContext.setIgnoreNotificationFailure(true); + + notificationContext.setBodyTemplate(supersededTemplate); + Map args = new HashMap(1, 1.0f); + args.put("record", record); + args.put("site", getSiteName(root)); + notificationContext.setTemplateArgs(args); + + String groupName = getGroupName(root); + notificationContext.addTo(groupName); + + notificationService.sendNotification(EMailNotificationProvider.NAME, notificationContext); + } + + /** + * Gets the rm root given a context node. + * + * @param context context node reference + * @return {@link NodeRef} rm root node reference + */ + private NodeRef getRMRoot(final NodeRef context) + { + return AuthenticationUtil.runAs(new RunAsWork() + { + @Override + public NodeRef doWork() throws Exception + { + return recordsManagementService.getFilePlan(context); + + } + }, AuthenticationUtil.getSystemUserName()); + + } + + /** + * Gets the group name for the notification role. + * + * @param root rm root node + * @return String notification role's group name + */ + private String getGroupName(final NodeRef root) + { + return AuthenticationUtil.runAs(new RunAsWork() + { + @Override + public String doWork() throws Exception + { + // Find the authority for the given role + Role role = securityService.getRole(root, notificationRole); + return role.getRoleGroupName(); + } + }, AuthenticationUtil.getSystemUserName()); + } + + /** + * Get the site name, default if none/undetermined. + * + * @param root rm root + * @return String site name + */ + private String getSiteName(final NodeRef root) + { + return AuthenticationUtil.runAs(new RunAsWork() + { + @Override + public String doWork() throws Exception + { + String result = DEFAULT_SITE; + + SiteInfo siteInfo = siteService.getSite(root); + if (siteInfo != null) + { + result = siteInfo.getShortName(); + } + + return result; + } + }, AuthenticationUtil.getSystemUserName()); + + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/patch/NotificationTemplatePatch.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/patch/NotificationTemplatePatch.java new file mode 100644 index 0000000000..11c49ea482 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/patch/NotificationTemplatePatch.java @@ -0,0 +1,187 @@ +/* + * 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.module.org_alfresco_module_rm.patch; + +import java.io.InputStream; +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.notification.RecordsManagementNotificationHelper; +import org.alfresco.repo.module.AbstractModuleComponent; +import org.alfresco.repo.version.VersionModel; +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; +import org.alfresco.service.cmr.version.Version; +import org.alfresco.service.cmr.version.VersionService; +import org.alfresco.service.cmr.version.VersionType; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.BeanNameAware; + +/** + * @author Roy Wetherall + */ +public class NotificationTemplatePatch extends AbstractModuleComponent + implements BeanNameAware +{ + /** Last patch update property */ + private static final QName PROP_LAST_PATCH_UPDATE = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "lastPatchUpdate"); + + private static final String PATH_DUE_FOR_REVIEW = "alfresco/module/org_alfresco_module_rm/bootstrap/content/notify-records-due-for-review-email.ftl"; + private static final String PATH_SUPERSEDED = "alfresco/module/org_alfresco_module_rm/bootstrap/content/record-superseded-email.ftl"; + + /** Logger */ + private static Log logger = LogFactory.getLog(NotificationTemplatePatch.class); + + /** Records management notification helper */ + private RecordsManagementNotificationHelper notificationHelper; + + /** Node service */ + private NodeService nodeService; + + /** Content service */ + private ContentService contentService; + + /** Version service */ + private VersionService versionService; + + /** Bean name */ + private String name; + + /** + * @param notificationHelper notification helper + */ + public void setNotificationHelper(RecordsManagementNotificationHelper notificationHelper) + { + this.notificationHelper = notificationHelper; + } + + /** + * @param nodeService node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * @param contentService content service + */ + public void setContentService(ContentService contentService) + { + this.contentService = contentService; + } + + /** + * @param versionService version service + */ + public void setVersionService(VersionService versionService) + { + this.versionService = versionService; + } + + /** + * @see org.alfresco.repo.module.AbstractModuleComponent#setBeanName(java.lang.String) + */ + @Override + public void setBeanName(String name) + { + this.name = name; + } + + /** + * @see org.alfresco.repo.module.AbstractModuleComponent#executeInternal() + */ + @Override + protected void executeInternal() throws Throwable + { + if (logger.isDebugEnabled() == true) + { + logger.debug("RM Module NotificationTemplatePatch ..."); + } + + NodeRef supersededTemplate = notificationHelper.getSupersededTemplate(); + updateTemplate(supersededTemplate, PATH_SUPERSEDED); + + NodeRef dueForReviewTemplate = notificationHelper.getDueForReviewTemplate(); + updateTemplate(dueForReviewTemplate, PATH_DUE_FOR_REVIEW); + } + + /** + * Attempt to update the template with the updated version + * + * @param template + * @param updatedTemplate + */ + private void updateTemplate(NodeRef template, String templateUpdate) + { + if (template == null || nodeService.exists(template) == false) + { + if (logger.isDebugEnabled() == true) + { + logger.debug("Skipping template update, because template has not been bootstraped."); + } + } + else + { + // Check to see if this template has already been updated + String lastPatchUpdate = (String)nodeService.getProperty(template, PROP_LAST_PATCH_UPDATE); + if (lastPatchUpdate == null || name.equals(lastPatchUpdate) == false) + { + if (logger.isDebugEnabled() == true) + { + logger.debug("Applying update to template. (template=" + template.toString() + ", templateUpdate=" + templateUpdate + ")"); + } + + // Make sure the template is versionable + if (nodeService.hasAspect(template, ContentModel.ASPECT_VERSIONABLE) == false) + { + nodeService.addAspect(template, ContentModel.ASPECT_VERSIONABLE, null); + + // Create version (before template is updated) + Map versionProperties = new HashMap(2); + versionProperties.put(Version.PROP_DESCRIPTION, "Initial version"); + versionProperties.put(VersionModel.PROP_VERSION_TYPE, VersionType.MINOR); + versionService.createVersion(template, versionProperties); + } + + // Update the content of the template + InputStream is = getClass().getClassLoader().getResourceAsStream(templateUpdate); + ContentWriter writer = contentService.getWriter(template, ContentModel.PROP_CONTENT, true); + writer.putContent(is); + + // Set the last patch update property + nodeService.setProperty(template, PROP_LAST_PATCH_UPDATE, name); + } + else + { + if (logger.isDebugEnabled() == true) + { + logger.debug("Skipping template update, because template has already been patched. (template=" + template.toString() + ")"); + } + } + } + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/patch/RMv2ModelPatch.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/patch/RMv2ModelPatch.java new file mode 100644 index 0000000000..545d846d92 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/patch/RMv2ModelPatch.java @@ -0,0 +1,161 @@ +/* + * 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.module.org_alfresco_module_rm.patch; + +import java.util.List; + +import org.alfresco.module.org_alfresco_module_rm.dod5015.DOD5015Model; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.repo.domain.node.NodeDAO; +import org.alfresco.repo.domain.patch.PatchDAO; +import org.alfresco.repo.domain.qname.QNameDAO; +import org.alfresco.repo.module.AbstractModuleComponent; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.Pair; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.BeanNameAware; + +/** + * RM v2.0 Model Updates Patch + * + * + * @author Roy Wetherall + */ +public class RMv2ModelPatch extends AbstractModuleComponent + implements BeanNameAware, RecordsManagementModel, DOD5015Model +{ + /** Logger */ + private static Log logger = LogFactory.getLog(NotificationTemplatePatch.class); + + private static long BATCH_SIZE = 100000L; + + private PatchDAO patchDAO; + private NodeDAO nodeDAO; + private QNameDAO qnameDAO; + private RetryingTransactionHelper retryingTransactionHelper; + + public void setPatchDAO(PatchDAO patchDAO) + { + this.patchDAO = patchDAO; + } + + public void setNodeDAO(NodeDAO nodeDAO) + { + this.nodeDAO = nodeDAO; + } + + public void setQnameDAO(QNameDAO qnameDAO) + { + this.qnameDAO = qnameDAO; + } + + public void setRetryingTransactionHelper(RetryingTransactionHelper retryingTransactionHelper) + { + this.retryingTransactionHelper = retryingTransactionHelper; + } + + /** + * @see org.alfresco.repo.module.AbstractModuleComponent#executeInternal() + */ + @Override + protected void executeInternal() throws Throwable + { + if (logger.isDebugEnabled() == true) + { + logger.debug("RM Module NotificationTemplatePatch ..."); + } + + updateQName(QName.createQName(DOD_URI, "filePlan"), TYPE_FILE_PLAN, "TYPE"); + updateQName(QName.createQName(DOD_URI, "recordCategory"), TYPE_RECORD_CATEGORY, "TYPE"); + updateQName(QName.createQName(DOD_URI, "ghosted"), ASPECT_GHOSTED, "ASPECT"); + } + + private void updateQName(QName qnameBefore, QName qnameAfter, String reindexClass) + { + Long maxNodeId = patchDAO.getMaxAdmNodeID(); + + Pair before = qnameDAO.getQName(qnameBefore); + + if (before != null) + { + for (Long i = 0L; i < maxNodeId; i+=BATCH_SIZE) + { + Work work = new Work(before.getFirst(), i, reindexClass); + retryingTransactionHelper.doInTransaction(work, false, true); + } + + qnameDAO.updateQName(qnameBefore, qnameAfter); + + if (logger.isDebugEnabled() == true) + { + logger.debug(" ... updated qname " + qnameBefore.toString()); + } + } + else + { + if (logger.isDebugEnabled() == true) + { + logger.debug(" ... no need to update qname " + qnameBefore.toString()); + } + } + } + + private class Work implements RetryingTransactionHelper.RetryingTransactionCallback + { + private long qnameId; + private long lower; + private String reindexClass; + + Work(long qnameId, long lower, String reindexClass) + { + this.qnameId = qnameId; + this.lower = lower; + this.reindexClass = reindexClass; + } + + /* + * (non-Javadoc) + * @see org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback#execute() + */ + @Override + public Integer execute() throws Throwable + { + if ("TYPE".equals(reindexClass)) + { + List nodeIds = patchDAO.getNodesByTypeQNameId(qnameId, lower, lower + BATCH_SIZE); + nodeDAO.touchNodes(nodeDAO.getCurrentTransactionId(true), nodeIds); + return nodeIds.size(); + } + else if ("ASPECT".equals(reindexClass)) + { + List nodeIds = patchDAO.getNodesByAspectQNameId(qnameId, lower, lower + BATCH_SIZE); + nodeDAO.touchNodes(nodeDAO.getCurrentTransactionId(true), nodeIds); + return nodeIds.size(); + } + else + { + // nothing to do + return 0; + } + + } + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/AbstractRmWebScript.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/AbstractRmWebScript.java new file mode 100644 index 0000000000..315d16c0ca --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/AbstractRmWebScript.java @@ -0,0 +1,147 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService; +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.NamespaceService; +import org.json.JSONObject; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * + * @author Neil McErlean + */ +public abstract class AbstractRmWebScript extends DeclarativeWebScript +{ + protected NodeService nodeService; + protected RecordsManagementService rmService; + protected DispositionService dispositionService; + protected NamespaceService namespaceService; + + /** + * Parses the request and providing it's valid returns the NodeRef. + * + * @param req The webscript request + * @return The NodeRef passed in the request + * + * @author Gavin Cornwell + */ + protected NodeRef parseRequestForNodeRef(WebScriptRequest req) + { + // get the parameters that represent the NodeRef, 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"); + String nodeId = templateVars.get("id"); + + // create the NodeRef and ensure it is valid + StoreRef storeRef = new StoreRef(storeType, storeId); + NodeRef nodeRef = new NodeRef(storeRef, nodeId); + + if (!this.nodeService.exists(nodeRef)) + { + throw new WebScriptException(HttpServletResponse.SC_NOT_FOUND, "Unable to find node: " + + nodeRef.toString()); + } + + return nodeRef; + } + + /** + * Sets the RecordsManagementService instance + * + * @param rmService The RecordsManagementService instance + */ + public void setRecordsManagementService(RecordsManagementService rmService) + { + this.rmService = rmService; + } + + /** + * @param dispositionService the disposition serviceS + */ + public void setDispositionService(DispositionService dispositionService) + { + this.dispositionService = dispositionService; + } + + /** + * Sets the NodeService instance + * + * @param nodeService The NodeService instance + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Sets the NamespaceService instance + * + * @param namespaceService The NamespaceService instance + */ + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + /** + * This method checks if the json object contains an entry with the specified name. + * + * @param json the json object. + * @param paramName the name to check for. + * @throws WebScriptException if the specified entry is missing. + */ + protected void checkMandatoryJsonParam(JSONObject json, String paramName) + { + if (json.has(paramName) == false) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Mandatory '" + paramName + "' parameter was not provided in request body"); + } + } + + /** + * This method checks if the json object contains entries with the specified names. + * + * @param json the json object. + * @param paramNames the names to check for. + * @throws WebScriptException if any of the specified entries are missing. + */ + protected void checkMandatoryJsonParams(JSONObject json, List paramNames) + { + for (String name : paramNames) + { + this.checkMandatoryJsonParam(json, name); + } + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/ApplyDodCertModelFixesGet.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/ApplyDodCertModelFixesGet.java new file mode 100644 index 0000000000..5785be3ae3 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/ApplyDodCertModelFixesGet.java @@ -0,0 +1,210 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminServiceImpl; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementCustomModel; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.dictionary.IndexTokenisationMode; +import org.alfresco.repo.dictionary.M2Aspect; +import org.alfresco.repo.dictionary.M2ClassAssociation; +import org.alfresco.repo.dictionary.M2Model; +import org.alfresco.repo.dictionary.M2Property; +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.springframework.extensions.webscripts.Cache; +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; + +/** + * This webscript applies necessary changes to the RM custom model in the repository. These changes + * are to 'patch' a deployed RM custom model during the DoD certification process. With that in mind + * they are safe to apply to a live database i.e. without side-effect to existing data and safe + * to call multiple times. + *

+ * + * TODO This webscript should be removed after DOD certification as none of these patches are needed + * for a newly-installed DoD amp. + * + * @author neilm + */ +@Deprecated +public class ApplyDodCertModelFixesGet extends DeclarativeWebScript + implements RecordsManagementModel +{ + private static final NodeRef RM_CUSTOM_MODEL_NODE_REF = new NodeRef("workspace://SpacesStore/records_management_custom_model"); + private static final String RMC_CUSTOM_RECORD_SERIES_PROPERTIES = RecordsManagementCustomModel.RM_CUSTOM_PREFIX + ":customRecordSeriesProperties"; + private static final String RMC_CUSTOM_RECORD_CATEGORY_PROPERTIES = RecordsManagementCustomModel.RM_CUSTOM_PREFIX + ":customRecordCategoryProperties"; + private static final String RMC_CUSTOM_RECORD_FOLDER_PROPERTIES = RecordsManagementCustomModel.RM_CUSTOM_PREFIX + ":customRecordFolderProperties"; + private static final String RMC_CUSTOM_RECORD_PROPERTIES = RecordsManagementCustomModel.RM_CUSTOM_PREFIX + ":customRecordProperties"; + + /** Logger */ + private static Log logger = LogFactory.getLog(ApplyDodCertModelFixesGet.class); + + private ContentService contentService; + + public void setContentService(ContentService contentService) + { + this.contentService = contentService; + } + + @Override + public Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + if (logger.isInfoEnabled()) + { + logger.info("Applying webscript-based patches to RM custom model in the repo."); + } + + M2Model customModel = readCustomContentModel(); + + M2Aspect customAssocsAspect = customModel.getAspect(RecordsManagementAdminServiceImpl.RMC_CUSTOM_ASSOCS); + + if (customAssocsAspect == null) + { + final String msg = "Unknown aspect: "+RecordsManagementAdminServiceImpl.RMC_CUSTOM_ASSOCS; + if (logger.isErrorEnabled()) + { + logger.error(msg); + } + throw new AlfrescoRuntimeException(msg); + } + + + // MOB-1573. All custom references should have many-many multiplicity. + if (logger.isInfoEnabled()) + { + logger.info("MOB-1573. All custom references should have many-many multiplicity."); + } + + for (M2ClassAssociation classAssoc : customAssocsAspect.getAssociations()) + { + classAssoc.setSourceMany(true); + classAssoc.setTargetMany(true); + + } + + + + //MOB-1621. Custom fields should be created as untokenized by default. + if (logger.isInfoEnabled()) + { + logger.info("MOB-1621. Custom fields should be created as untokenized by default."); + } + + List allCustomPropertiesAspects = new ArrayList(4); + allCustomPropertiesAspects.add(RMC_CUSTOM_RECORD_SERIES_PROPERTIES); + allCustomPropertiesAspects.add(RMC_CUSTOM_RECORD_CATEGORY_PROPERTIES); + allCustomPropertiesAspects.add(RMC_CUSTOM_RECORD_FOLDER_PROPERTIES); + allCustomPropertiesAspects.add(RMC_CUSTOM_RECORD_PROPERTIES); + for (String aspectName : allCustomPropertiesAspects) + { + M2Aspect aspectObj = customModel.getAspect(aspectName); + List customProperties = aspectObj.getProperties(); + for (M2Property propertyObj : customProperties) + { + propertyObj.setIndexed(true); + propertyObj.setIndexedAtomically(true); + propertyObj.setStoredInIndex(false); + propertyObj.setIndexTokenisationMode(IndexTokenisationMode.FALSE); + } + } + + + writeCustomContentModel(customModel); + + if (logger.isInfoEnabled()) + { + logger.info("Completed application of webscript-based patches to RM custom model in the repo."); + } + + Map model = new HashMap(1, 1.0f); + model.put("success", true); + + return model; + } + + private M2Model readCustomContentModel() + { + ContentReader reader = this.contentService.getReader(RM_CUSTOM_MODEL_NODE_REF, + ContentModel.TYPE_CONTENT); + + if (reader.exists() == false) {throw new AlfrescoRuntimeException("RM CustomModel has no content.");} + + InputStream contentIn = null; + M2Model deserializedModel = null; + try + { + contentIn = reader.getContentInputStream(); + deserializedModel = M2Model.createModel(contentIn); + } + finally + { + try + { + if (contentIn != null) contentIn.close(); + } + catch (IOException ignored) + { + // Intentionally empty.` + } + } + return deserializedModel; + } + + private void writeCustomContentModel(M2Model deserializedModel) + { + ContentWriter writer = this.contentService.getWriter(RM_CUSTOM_MODEL_NODE_REF, + ContentModel.TYPE_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_XML); + writer.setEncoding("UTF-8"); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + deserializedModel.toXML(baos); + + String updatedModelXml; + try + { + updatedModelXml = baos.toString("UTF-8"); + writer.putContent(updatedModelXml); + // putContent closes all resources. + // so we don't have to. + } catch (UnsupportedEncodingException uex) + { + throw new AlfrescoRuntimeException("Exception when writing custom model xml.", uex); + } + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/ApplyFixMob1573Get.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/ApplyFixMob1573Get.java new file mode 100644 index 0000000000..7aee420818 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/ApplyFixMob1573Get.java @@ -0,0 +1,146 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminServiceImpl; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.dictionary.M2Aspect; +import org.alfresco.repo.dictionary.M2ClassAssociation; +import org.alfresco.repo.dictionary.M2Model; +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.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This webscript patches the RM custom model as fix for MOB-1573. It is only necessary for databases + * that had their RM amps initialised before the fix went in. + * There is no side-effect if it is called when it is not needed or if it is called multiple times. + * + * TODO This webscript should be removed after DOD certification. + * + * @author neilm + */ +@Deprecated +public class ApplyFixMob1573Get extends DeclarativeWebScript + implements RecordsManagementModel +{ + private static final NodeRef RM_CUSTOM_MODEL_NODE_REF = new NodeRef("workspace://SpacesStore/records_management_custom_model"); + + private ContentService contentService; + + public void setContentService(ContentService contentService) + { + this.contentService = contentService; + } + + @Override + public Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + M2Model customModel = readCustomContentModel(); + + // Go through every custom reference defined in the custom model and make sure that it + // has many-to-many multiplicity + String aspectName = RecordsManagementAdminServiceImpl.RMC_CUSTOM_ASSOCS; + M2Aspect customAssocsAspect = customModel.getAspect(aspectName); + + if (customAssocsAspect == null) + { + throw new AlfrescoRuntimeException("Unknown aspect: "+aspectName); + } + + for (M2ClassAssociation classAssoc : customAssocsAspect.getAssociations()) + { + classAssoc.setSourceMany(true); + classAssoc.setTargetMany(true); + } + + writeCustomContentModel(customModel); + + Map model = new HashMap(1, 1.0f); + model.put("success", true); + + return model; + } + + private M2Model readCustomContentModel() + { + ContentReader reader = this.contentService.getReader(RM_CUSTOM_MODEL_NODE_REF, + ContentModel.TYPE_CONTENT); + + if (reader.exists() == false) {throw new AlfrescoRuntimeException("RM CustomModel has no content.");} + + InputStream contentIn = null; + M2Model deserializedModel = null; + try + { + contentIn = reader.getContentInputStream(); + deserializedModel = M2Model.createModel(contentIn); + } + finally + { + try + { + if (contentIn != null) contentIn.close(); + } + catch (IOException ignored) + { + // Intentionally empty.` + } + } + return deserializedModel; + } + + private void writeCustomContentModel(M2Model deserializedModel) + { + ContentWriter writer = this.contentService.getWriter(RM_CUSTOM_MODEL_NODE_REF, + ContentModel.TYPE_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_XML); + writer.setEncoding("UTF-8"); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + deserializedModel.toXML(baos); + + String updatedModelXml; + try + { + updatedModelXml = baos.toString("UTF-8"); + writer.putContent(updatedModelXml); + // putContent closes all resources. + // so we don't have to. + } catch (UnsupportedEncodingException uex) + { + throw new AlfrescoRuntimeException("Exception when writing custom model xml.", uex); + } + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/AuditLogDelete.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/AuditLogDelete.java new file mode 100644 index 0000000000..2330b96fb9 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/AuditLogDelete.java @@ -0,0 +1,49 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Implementation for Java backed webscript to clear the + * Records Management audit log. + * + * @author Gavin Cornwell + */ +public class AuditLogDelete extends BaseAuditAdminWebScript +{ + /* + * @see org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.Status, org.alfresco.web.scripts.Cache) + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + this.rmAuditService.clear(); + + // create model object with the audit status model + Map model = new HashMap(1); + model.put("auditstatus", createAuditStatusModel()); + return model; + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/AuditLogGet.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/AuditLogGet.java new file mode 100644 index 0000000000..fae34d324a --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/AuditLogGet.java @@ -0,0 +1,88 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +import java.io.File; +import java.io.IOException; + +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Implementation for Java backed webscript to return audit + * log of RM events, optionally scoped to an RM node. + * + * @author Gavin Cornwell + */ +public class AuditLogGet extends BaseAuditRetrievalWebScript +{ + /** Logger */ + private static Log logger = LogFactory.getLog(AuditLogGet.class); + + protected final static String PARAM_EXPORT = "export"; + + @Override + public void execute(WebScriptRequest req, WebScriptResponse res) throws IOException + { + File auditTrail = null; + + try + { + // parse the parameters and get a file containing the audit trail + auditTrail = this.rmAuditService.getAuditTrailFile(parseQueryParameters(req), parseReportFormat(req)); + + if (logger.isDebugEnabled()) + logger.debug("Streaming audit trail from file: " + auditTrail.getAbsolutePath()); + + boolean attach = false; + String attachFileName = null; + String export = req.getParameter(PARAM_EXPORT); + if (export != null && Boolean.parseBoolean(export)) + { + attach = true; + attachFileName = auditTrail.getName(); + + if (logger.isDebugEnabled()) + logger.debug("Exporting audit trail using file name: " + attachFileName); + } + + // stream the file back to the client + streamContent(req, res, auditTrail, attach, attachFileName); + } + finally + { + if (auditTrail != null) + { + if (logger.isDebugEnabled()) + { + logger.debug( + "Audit results written to file: \n" + + " File: " + auditTrail + "\n" + + " Parameter: " + parseQueryParameters(req)); + } + else + { + auditTrail.delete(); + } + } + } + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/AuditLogPost.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/AuditLogPost.java new file mode 100644 index 0000000000..f4cd3bd32f --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/AuditLogPost.java @@ -0,0 +1,139 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.service.cmr.repository.NodeRef; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONObject; +import org.json.JSONTokener; + +/** + * Implementation for Java backed webscript to file an + * audit log as a record. + * + * @author Gavin Cornwell + */ +public class AuditLogPost extends BaseAuditRetrievalWebScript +{ + /** Logger */ + private static Log logger = LogFactory.getLog(AuditLogPost.class); + + protected static final String PARAM_DESTINATION = "destination"; + protected static final String RESPONSE_SUCCESS = "success"; + protected static final String RESPONSE_RECORD = "record"; + protected static final String RESPONSE_RECORD_NAME = "recordName"; + + @Override + public void execute(WebScriptRequest req, WebScriptResponse res) throws IOException + { + try + { + // retrieve requested format + String format = req.getFormat(); + + // construct model for template + Status status = new Status(); + Cache cache = new Cache(getDescription().getRequiredCache()); + Map model = new HashMap(); + model.put("status", status); + model.put("cache", cache); + + // extract the destination parameter, ensure it's present and it is + // a record folder + JSONObject json = new JSONObject(new JSONTokener(req.getContent().getContent())); + if (!json.has(PARAM_DESTINATION)) + { + status.setCode(HttpServletResponse.SC_BAD_REQUEST, + "Mandatory '" + PARAM_DESTINATION + "' parameter has not been supplied"); + Map templateModel = createTemplateParameters(req, res, model); + sendStatus(req, res, status, cache, format, templateModel); + return; + } + + String destinationParam = json.getString(PARAM_DESTINATION); + NodeRef destination = new NodeRef(destinationParam); + + if (!this.nodeService.exists(destination)) + { + status.setCode(HttpServletResponse.SC_NOT_FOUND, + "Node " + destination.toString() + " does not exist"); + Map templateModel = createTemplateParameters(req, res, model); + sendStatus(req, res, status, cache, format, templateModel); + return; + } + + // ensure the node is a filePlan object + if (!RecordsManagementModel.TYPE_RECORD_FOLDER.equals(this.nodeService.getType(destination))) + { + status.setCode(HttpServletResponse.SC_BAD_REQUEST, + "Node " + destination.toString() + " is not a record folder"); + Map templateModel = createTemplateParameters(req, res, model); + sendStatus(req, res, status, cache, format, templateModel); + return; + } + + if (logger.isDebugEnabled()) + logger.debug("Filing audit trail as record in record folder: " + destination); + + // parse the other parameters and get a file containing the audit trail + NodeRef record = this.rmAuditService.fileAuditTrailAsRecord(parseQueryParameters(req), + destination, parseReportFormat(req)); + + if (logger.isDebugEnabled()) + logger.debug("Filed audit trail as new record: " + record); + + // return success flag and record noderef as JSON + JSONObject responseJSON = new JSONObject(); + responseJSON.put(RESPONSE_SUCCESS, (record != null)); + if (record != null) + { + responseJSON.put(RESPONSE_RECORD, record.toString()); + responseJSON.put(RESPONSE_RECORD_NAME, + (String)nodeService.getProperty(record, ContentModel.PROP_NAME)); + } + + // setup response + String jsonString = responseJSON.toString(); + res.setContentType(MimetypeMap.MIMETYPE_JSON); + res.setContentEncoding("UTF-8"); + res.setHeader("Content-Length", Long.toString(jsonString.length())); + + // write the JSON response + res.getWriter().write(jsonString); + } + catch (Throwable e) + { + throw createStatusException(e, req, res); + } + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/AuditLogPut.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/AuditLogPut.java new file mode 100644 index 0000000000..81cdb7e182 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/AuditLogPut.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.module.org_alfresco_module_rm.script; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; + +/** + * Implementation for Java backed webscript to start + * and stop Records Management auditing. + * + * @author Gavin Cornwell + */ +public class AuditLogPut extends BaseAuditAdminWebScript +{ + protected static final String PARAM_ENABLED = "enabled"; + + /* + * @see org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.Status, org.alfresco.web.scripts.Cache) + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + try + { + // determine whether to start or stop auditing + JSONObject json = new JSONObject(new JSONTokener(req.getContent().getContent())); + + // check the enabled property present + if (!json.has(PARAM_ENABLED)) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Mandatory 'enabled' parameter was not provided in request body"); + } + + boolean enabled = json.getBoolean(PARAM_ENABLED); + if (enabled) + { + this.rmAuditService.start(); + } + else + { + this.rmAuditService.stop(); + } + + // create model object with the audit status model + Map model = new HashMap(1); + model.put("auditstatus", createAuditStatusModel()); + return model; + } + catch (IOException iox) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Could not read content from req.", iox); + } + catch (JSONException je) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Could not parse JSON from req.", je); + } + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/AuditLogStatusGet.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/AuditLogStatusGet.java new file mode 100644 index 0000000000..5afb650124 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/AuditLogStatusGet.java @@ -0,0 +1,60 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.audit.RecordsManagementAuditService; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * GET audit log status + * + * @author Roy Wetherall + */ +public class AuditLogStatusGet extends DeclarativeWebScript +{ + /** Records management audit service */ + protected RecordsManagementAuditService rmAuditService; + + /** + * Sets the RecordsManagementAuditService instance + * + * @param auditService The RecordsManagementAuditService instance + */ + public void setRecordsManagementAuditService(RecordsManagementAuditService rmAuditService) + { + this.rmAuditService = rmAuditService; + } + + /** + * @see org.alfresco.repo.web.scripts.content.StreamContent#executeImpl(org.springframework.extensions.webscripts.WebScriptRequest, org.springframework.extensions.webscripts.Status, org.springframework.extensions.webscripts.Cache) + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(1); + model.put("enabled", Boolean.valueOf(rmAuditService.isEnabled())); + return model; + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/BaseAuditAdminWebScript.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/BaseAuditAdminWebScript.java new file mode 100644 index 0000000000..8f5d97a687 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/BaseAuditAdminWebScript.java @@ -0,0 +1,62 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.audit.RecordsManagementAuditService; +import org.springframework.extensions.surf.util.ISO8601DateFormat; +import org.springframework.extensions.webscripts.DeclarativeWebScript; + +/** + * Base class for all audit administration webscripts. + * + * @author Gavin Cornwell + */ +public class BaseAuditAdminWebScript extends DeclarativeWebScript +{ + protected RecordsManagementAuditService rmAuditService; + + /** + * Sets the RecordsManagementAuditService instance + * + * @param auditService The RecordsManagementAuditService instance + */ + public void setRecordsManagementAuditService(RecordsManagementAuditService rmAuditService) + { + this.rmAuditService = rmAuditService; + } + + /** + * Creates a model to represent the current status of the RM audit log. + * + * @return Map of RM audit log status + */ + protected Map createAuditStatusModel() + { + Map auditStatus = new HashMap(3); + + auditStatus.put("started", ISO8601DateFormat.format(rmAuditService.getDateLastStarted())); + auditStatus.put("stopped", ISO8601DateFormat.format(rmAuditService.getDateLastStopped())); + auditStatus.put("enabled", Boolean.valueOf(rmAuditService.isEnabled())); + + return auditStatus; + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/BaseAuditRetrievalWebScript.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/BaseAuditRetrievalWebScript.java new file mode 100644 index 0000000000..bcdda05e1b --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/BaseAuditRetrievalWebScript.java @@ -0,0 +1,249 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +import java.io.IOException; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.audit.RecordsManagementAuditQueryParameters; +import org.alfresco.module.org_alfresco_module_rm.audit.RecordsManagementAuditService; +import org.alfresco.module.org_alfresco_module_rm.audit.RecordsManagementAuditService.ReportFormat; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.web.scripts.content.StreamContent; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.InvalidQNameException; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; + +/** + * Base class for all audit retrieval webscripts. + * + * @author Gavin Cornwell + */ +public class BaseAuditRetrievalWebScript extends StreamContent +{ + /** Logger */ + private static Log logger = LogFactory.getLog(BaseAuditRetrievalWebScript.class); + + protected final static String PARAM_USER = "user"; + protected final static String PARAM_SIZE = "size"; + protected final static String PARAM_EVENT = "event"; + protected final static String PARAM_FROM = "from"; + protected final static String PARAM_TO = "to"; + protected final static String PARAM_PROPERTY = "property"; + protected final static String DATE_PATTERN = "yyyy-MM-dd"; + + protected RecordsManagementAuditService rmAuditService; + + /** + * Sets the RecordsManagementAuditService instance + * + * @param auditService The RecordsManagementAuditService instance + */ + public void setRecordsManagementAuditService(RecordsManagementAuditService rmAuditService) + { + this.rmAuditService = rmAuditService; + } + + /** + * Parses the given request and builds an instance of + * RecordsManagementAuditQueryParameters to retrieve the relevant audit entries + * + * @param req The request + * @return RecordsManagementAuditQueryParameters instance + */ + protected RecordsManagementAuditQueryParameters parseQueryParameters(WebScriptRequest req) + { + // create parameters for audit trail retrieval + RecordsManagementAuditQueryParameters params = new RecordsManagementAuditQueryParameters(); + + // the webscripts can have a couple of different forms of url, work out + // whether a nodeRef has been supplied or whether the whole audit + // log should be displayed + NodeRef nodeRef = null; + Map templateVars = req.getServiceMatch().getTemplateVars(); + String storeType = templateVars.get("store_type"); + if (storeType != null && storeType.length() > 0) + { + // there is a store_type so all other params are likely to be present + String storeId = templateVars.get("store_id"); + String nodeId = templateVars.get("id"); + + // create the nodeRef + nodeRef = new NodeRef(new StoreRef(storeType, storeId), nodeId); + } + + // gather all the common filtering parameters, these could be on the + // query string, in a multipart/form-data request or in a JSON body + String size = null; + String user = null; + String event = null; + String from = null; + String to = null; + String property = null; + + if (MimetypeMap.MIMETYPE_JSON.equals(req.getContentType())) + { + try + { + JSONObject json = new JSONObject(new JSONTokener(req.getContent().getContent())); + if (json.has(PARAM_SIZE)) + { + size = json.getString(PARAM_SIZE); + } + if (json.has(PARAM_USER)) + { + user = json.getString(PARAM_USER); + } + if (json.has(PARAM_EVENT)) + { + event = json.getString(PARAM_EVENT); + } + if (json.has(PARAM_FROM)) + { + from = json.getString(PARAM_FROM); + } + if (json.has(PARAM_TO)) + { + to = json.getString(PARAM_TO); + } + if (json.has(PARAM_PROPERTY)) + { + property = json.getString(PARAM_PROPERTY); + } + } + catch (IOException ioe) + { + // log a warning + if (logger.isWarnEnabled()) + logger.warn("Failed to parse JSON parameters for audit query: " + ioe.getMessage()); + } + catch (JSONException je) + { + // log a warning + if (logger.isWarnEnabled()) + logger.warn("Failed to parse JSON parameters for audit query: " + je.getMessage()); + } + } + else + { + size = req.getParameter(PARAM_SIZE); + user = req.getParameter(PARAM_USER); + event = req.getParameter(PARAM_EVENT); + from = req.getParameter(PARAM_FROM); + to = req.getParameter(PARAM_TO); + property = req.getParameter(PARAM_PROPERTY); + } + + // setup the audit query parameters object + params.setNodeRef(nodeRef); + params.setUser(user); + params.setEvent(event); + + if (size != null && size.length() > 0) + { + try + { + params.setMaxEntries(Integer.parseInt(size)); + } + catch (NumberFormatException nfe) + { + if (logger.isWarnEnabled()) + logger.warn("Ignoring size parameter as '" + size + "' is not a number!"); + } + } + + if (from != null && from.length() > 0) + { + try + { + SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_PATTERN); + params.setDateFrom(dateFormat.parse(from)); + } + catch (ParseException pe) + { + if (logger.isWarnEnabled()) + logger.warn("Ignoring from parameter as '" + from + "' does not conform to the date pattern: " + DATE_PATTERN); + } + } + + if (to != null && to.length() > 0) + { + try + { + SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_PATTERN); + params.setDateTo(dateFormat.parse(to)); + } + catch (ParseException pe) + { + if (logger.isWarnEnabled()) + logger.warn("Ignoring to parameter as '" + to + "' does not conform to the date pattern: " + DATE_PATTERN); + } + } + + if (property != null && property.length() > 0) + { + try + { + params.setProperty(QName.createQName(property)); + } + catch (InvalidQNameException iqe) + { + if (logger.isWarnEnabled()) + logger.warn("Ignoring property parameter as '" + property + "' is an invalid QName"); + } + } + + return params; + } + + /** + * Parses the given request for the format the audit report + * should be returned in + * + * @param req The request + * @return The format for the report + */ + protected ReportFormat parseReportFormat(WebScriptRequest req) + { + String format = req.getFormat(); + + if (format != null) + { + if (format.equalsIgnoreCase("json")) + { + return ReportFormat.JSON; + } + else if (format.equalsIgnoreCase("html")) + { + return ReportFormat.HTML; + } + } + + return ReportFormat.JSON; + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/BaseCustomPropertyWebScript.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/BaseCustomPropertyWebScript.java new file mode 100644 index 0000000000..e53ca97bf8 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/BaseCustomPropertyWebScript.java @@ -0,0 +1,64 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +import org.alfresco.module.org_alfresco_module_rm.dod5015.DOD5015Model; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.service.namespace.QName; + +/** + * Base class for all custom property webscripts. + * + * @author Roy Wetherall + */ +public class BaseCustomPropertyWebScript extends AbstractRmWebScript +{ + /** + * Takes the element name and maps it to the QName of the customisable type. The passed element name should be a prefixed + * qname string, but to support previous versions of this API a couple of hard coded checks are made first. + * + * @param elementName + * @return + */ + protected QName mapToTypeQName(String elementName) + { + // Direct matching provided for backward compatibility with RM 1.0 + if ("recordFolder".equalsIgnoreCase(elementName) == true) + { + return RecordsManagementModel.TYPE_RECORD_FOLDER; + } + else if ("record".equalsIgnoreCase(elementName) == true) + { + return RecordsManagementModel.ASPECT_RECORD; + } + else if ("recordCategory".equalsIgnoreCase(elementName) == true) + { + return RecordsManagementModel.TYPE_RECORD_CATEGORY; + } + else if ("recordSeries".equalsIgnoreCase(elementName) == true) + { + return DOD5015Model.TYPE_RECORD_SERIES; + } + else + { + // Try and convert the string to a qname + return QName.createQName(elementName, namespaceService); + } + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/BaseTransferWebScript.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/BaseTransferWebScript.java new file mode 100644 index 0000000000..b37d2e6c9f --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/BaseTransferWebScript.java @@ -0,0 +1,202 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.repo.web.scripts.content.StreamACP; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.RegexQNamePattern; +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; +import org.springframework.extensions.webscripts.WebScriptResponse; + +/** + * Abstract base class for transfer related web scripts. + * + * @author Gavin Cornwell + */ +public abstract class BaseTransferWebScript extends StreamACP + implements RecordsManagementModel +{ + /** Logger */ + private static Log logger = LogFactory.getLog(BaseTransferWebScript.class); + + /** + * @see org.alfresco.web.scripts.WebScript#execute(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.WebScriptResponse) + */ + public void execute(WebScriptRequest req, WebScriptResponse res) throws IOException + { + File tempFile = null; + try + { + // retrieve requested format + String format = req.getFormat(); + + // construct model for template + Status status = new Status(); + Cache cache = new Cache(getDescription().getRequiredCache()); + Map model = new HashMap(); + model.put("status", status); + model.put("cache", cache); + + // get the parameters that represent the NodeRef, 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"); + String nodeId = templateVars.get("id"); + String transferId = templateVars.get("transfer_id"); + + // create and return the file plan NodeRef + NodeRef filePlan = new NodeRef(new StoreRef(storeType, storeId), nodeId); + + if (logger.isDebugEnabled()) + logger.debug("Retrieving transfer '" + transferId + "' from file plan: " + filePlan); + + // ensure the file plan exists + if (!this.nodeService.exists(filePlan)) + { + status.setCode(HttpServletResponse.SC_NOT_FOUND, + "Node " + filePlan.toString() + " does not exist"); + Map templateModel = createTemplateParameters(req, res, model); + sendStatus(req, res, status, cache, format, templateModel); + return; + } + + // ensure the node is a filePlan object + if (!TYPE_FILE_PLAN.equals(this.nodeService.getType(filePlan))) + { + status.setCode(HttpServletResponse.SC_BAD_REQUEST, + "Node " + filePlan.toString() + " is not a file plan"); + Map templateModel = createTemplateParameters(req, res, model); + sendStatus(req, res, status, cache, format, templateModel); + return; + } + + // attempt to find the transfer node + NodeRef transferNode = findTransferNode(filePlan, transferId); + + // send 404 if the transfer is not found + if (transferNode == null) + { + status.setCode(HttpServletResponse.SC_NOT_FOUND, + "Could not locate transfer with id: " + transferId); + Map templateModel = createTemplateParameters(req, res, model); + sendStatus(req, res, status, cache, format, templateModel); + return; + } + + // execute the transfer operation + tempFile = executeTransfer(transferNode, req, res, status, cache); + } + catch (Throwable e) + { + throw createStatusException(e, req, res); + } + finally + { + // try and delete the temporary file (if not in debug mode) + if (tempFile != null) + { + if (logger.isDebugEnabled()) + { + logger.debug("Transfer report saved to temporary file: " + tempFile.getAbsolutePath()); + } + else + { + tempFile.delete(); + } + } + } + } + + /** + * Abstract method subclasses implement to perform the actual logic required. + * + * @param transferNode The transfer node + * @param req The request + * @param res The response + * @param status Status object + * @param cache Cache object + * @return File object representing the file containing the JSON of the report + * @throws IOException + */ + protected abstract File executeTransfer(NodeRef transferNode, + WebScriptRequest req, WebScriptResponse res, + Status status, Cache cache) throws IOException; + + /** + * Finds a transfer object with the given id in the given file plan. + * This method returns null if a transfer with the given id is not found. + * + * @param filePlan The file plan to search + * @param transferId The id of the transfer being requested + * @return The transfer node or null if not found + */ + protected NodeRef findTransferNode(NodeRef filePlan, String transferId) + { + NodeRef transferNode = null; + + // get all the transfer nodes and find the one we need + List assocs = this.nodeService.getChildAssocs(filePlan, + RecordsManagementModel.ASSOC_TRANSFERS, RegexQNamePattern.MATCH_ALL); + for (ChildAssociationRef child : assocs) + { + if (child.getChildRef().getId().equals(transferId)) + { + transferNode = child.getChildRef(); + break; + } + } + + return transferNode; + } + + /** + * Returns an array of NodeRefs representing the items to be transferred. + * + * @param transferNode The transfer object + * @return Array of NodeRefs + */ + protected NodeRef[] getTransferNodes(NodeRef transferNode) + { + List assocs = this.nodeService.getChildAssocs(transferNode, + RecordsManagementModel.ASSOC_TRANSFERRED, RegexQNamePattern.MATCH_ALL); + NodeRef[] itemsToTransfer = new NodeRef[assocs.size()]; + for (int idx = 0; idx < assocs.size(); idx++) + { + itemsToTransfer[idx] = assocs.get(idx).getChildRef(); + } + + return itemsToTransfer; + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/BootstrapTestDataGet.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/BootstrapTestDataGet.java new file mode 100644 index 0000000000..a0571bf0d5 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/BootstrapTestDataGet.java @@ -0,0 +1,325 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService; +import org.alfresco.module.org_alfresco_module_rm.capability.RMPermissionModel; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementSearchBehaviour; +import org.alfresco.module.org_alfresco_module_rm.model.RmSiteType; +import org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService; +import org.alfresco.module.org_alfresco_module_rm.security.Role; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +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.ResultSet; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.cmr.security.AuthorityService; +import org.alfresco.service.cmr.security.AuthorityType; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.cmr.site.SiteInfo; +import org.alfresco.service.cmr.site.SiteService; +import org.alfresco.service.cmr.view.ImporterService; +import org.alfresco.service.cmr.view.Location; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * BootstrapTestData GET WebScript implementation. + */ +public class BootstrapTestDataGet extends DeclarativeWebScript + implements RecordsManagementModel +{ + private static Log logger = LogFactory.getLog(BootstrapTestDataGet.class); + + private static final String ARG_SITE_NAME = "site"; + private static final String ARG_IMPORT = "import"; + + private static final String XML_IMPORT = "alfresco/module/org_alfresco_module_rm/dod5015/DODExampleFilePlan.xml"; + + private static final StoreRef SPACES_STORE = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "SpacesStore"); + + private NodeService nodeService; + private SearchService searchService; + private RecordsManagementService recordsManagementService; + private RecordsManagementActionService recordsManagementActionService; + private ImporterService importerService; + private SiteService siteService; + private PermissionService permissionService; + private RecordsManagementSecurityService recordsManagementSecurityService; + private AuthorityService authorityService; + private RecordsManagementSearchBehaviour recordsManagementSearchBehaviour; + private DispositionService dispositionService; + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setSearchService(SearchService searchService) + { + this.searchService = searchService; + } + + public void setDispositionService(DispositionService dispositionService) + { + this.dispositionService = dispositionService; + } + + public void setRecordsManagementService(RecordsManagementService recordsManagementService) + { + this.recordsManagementService = recordsManagementService; + } + + public void setRecordsManagementActionService(RecordsManagementActionService recordsManagementActionService) + { + this.recordsManagementActionService = recordsManagementActionService; + } + + public void setImporterService(ImporterService importerService) + { + this.importerService = importerService; + } + + public void setSiteService(SiteService siteService) + { + this.siteService = siteService; + } + + public void setPermissionService(PermissionService permissionService) + { + this.permissionService = permissionService; + } + + public void setAuthorityService(AuthorityService authorityService) + { + this.authorityService = authorityService; + } + + public void setRecordsManagementSecurityService(RecordsManagementSecurityService recordsManagementSecurityService) + { + this.recordsManagementSecurityService = recordsManagementSecurityService; + } + + public void setRecordsManagementSearchBehaviour(RecordsManagementSearchBehaviour searchBehaviour) + { + this.recordsManagementSearchBehaviour = searchBehaviour; + } + + @Override + public Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + // resolve import argument + boolean importData = false; + if (req.getParameter(ARG_IMPORT) != null) + { + importData = Boolean.parseBoolean(req.getParameter(ARG_IMPORT)); + } + + // resolve rm site + String siteName = RmSiteType.DEFAULT_SITE_NAME; + if (req.getParameter(ARG_SITE_NAME) != null) + { + siteName = req.getParameter(ARG_SITE_NAME); + } + + if (importData) + { + SiteInfo site = siteService.getSite(siteName); + if (site == null) + { + throw new AlfrescoRuntimeException("Records Management site does not exist: " + siteName); + } + + // resolve documentLibrary (filePlan) container + NodeRef filePlan = siteService.getContainer(siteName, RmSiteType.COMPONENT_DOCUMENT_LIBRARY); + if (filePlan == null) + { + filePlan = siteService.createContainer(siteName, RmSiteType.COMPONENT_DOCUMENT_LIBRARY, TYPE_FILE_PLAN, null); + } + + // import the RM test data ACP into the the provided filePlan node reference + InputStream is = BootstrapTestDataGet.class.getClassLoader().getResourceAsStream(XML_IMPORT); + if (is == null) + { + throw new AlfrescoRuntimeException("The DODExampleFilePlan.xml import file could not be found"); + } + Reader viewReader = new InputStreamReader(is); + Location location = new Location(filePlan); + importerService.importView(viewReader, location, null, null); + } + + // Patch data + BootstrapTestDataGet.patchLoadedData(searchService, nodeService, recordsManagementService, + recordsManagementActionService, permissionService, + authorityService, recordsManagementSecurityService, + recordsManagementSearchBehaviour, + dispositionService); + + Map model = new HashMap(1, 1.0f); + model.put("success", true); + + return model; + } + + /** + * Temp method to patch AMP'ed data + * + * @param searchService + * @param nodeService + * @param recordsManagementService + * @param recordsManagementActionService + */ + public static void patchLoadedData( final SearchService searchService, + final NodeService nodeService, + final RecordsManagementService recordsManagementService, + final RecordsManagementActionService recordsManagementActionService, + final PermissionService permissionService, + final AuthorityService authorityService, + final RecordsManagementSecurityService recordsManagementSecurityService, + final RecordsManagementSearchBehaviour recordManagementSearchBehaviour, + final DispositionService dispositionService) + { + AuthenticationUtil.RunAsWork runAsWork = new AuthenticationUtil.RunAsWork() + { + public Object doWork() throws Exception + { + java.util.List rmRoots = recordsManagementService.getFilePlans(); + logger.info("Bootstraping " + rmRoots.size() + " rm roots ..."); + for (NodeRef rmRoot : rmRoots) + { + if (permissionService.getInheritParentPermissions(rmRoot) == true) + { + logger.info("Updating permissions for rm root: " + rmRoot); + permissionService.setInheritParentPermissions(rmRoot, false); + } + + String allRoleShortName = "AllRoles" + rmRoot.getId(); + String allRoleGroupName = authorityService.getName(AuthorityType.GROUP, allRoleShortName); + + if (authorityService.authorityExists(allRoleGroupName) == false) + { + logger.info("Creating all roles group for root node: " + rmRoot.toString()); + + // Create "all" role group for root node + String allRoles = authorityService.createAuthority(AuthorityType.GROUP, + allRoleShortName, + "All Roles", + null); + + // Put all the role groups in it + Set roles = recordsManagementSecurityService.getRoles(rmRoot); + for (Role role : roles) + { + logger.info(" - adding role group " + role.getRoleGroupName() + " to all roles group"); + authorityService.addAuthority(allRoles, role.getRoleGroupName()); + } + + // Set the permissions + permissionService.setPermission(rmRoot, allRoles, RMPermissionModel.READ_RECORDS, true); + } + } + + // Make sure all the containers do not inherit permissions + ResultSet rs = searchService.query(SPACES_STORE, SearchService.LANGUAGE_LUCENE, "TYPE:\"rma:recordsManagementContainer\""); + try + { + logger.info("Bootstraping " + rs.length() + " record containers ..."); + + for (NodeRef container : rs.getNodeRefs()) + { + String containerName = (String)nodeService.getProperty(container, ContentModel.PROP_NAME); + + // Set permissions + if (permissionService.getInheritParentPermissions(container) == true) + { + logger.info("Updating permissions for record container: " + containerName); + permissionService.setInheritParentPermissions(container, false); + } + } + } + finally + { + rs.close(); + } + + // fix up the test dataset to fire initial events for disposition schedules + rs = searchService.query(SPACES_STORE, SearchService.LANGUAGE_LUCENE, "TYPE:\"rma:recordFolder\""); + try + { + logger.info("Bootstraping " + rs.length() + " record folders ..."); + + for (NodeRef recordFolder : rs.getNodeRefs()) + { + String folderName = (String)nodeService.getProperty(recordFolder, ContentModel.PROP_NAME); + + // Set permissions + if (permissionService.getInheritParentPermissions(recordFolder) == true) + { + logger.info("Updating permissions for record folder: " + folderName); + permissionService.setInheritParentPermissions(recordFolder, false); + } + + if (nodeService.hasAspect(recordFolder, ASPECT_DISPOSITION_LIFECYCLE) == false) + { + // See if the folder has a disposition schedule that needs to be applied + DispositionSchedule ds = dispositionService.getDispositionSchedule(recordFolder); + if (ds != null) + { + // Fire action to "set-up" the folder correctly + logger.info("Setting up bootstraped record folder: " + folderName); + recordsManagementActionService.executeRecordsManagementAction(recordFolder, "setupRecordFolder"); + } + } + + // fixup the search behaviour aspect for the record folder + logger.info("Setting up search aspect for record folder: " + folderName); + recordManagementSearchBehaviour.fixupSearchAspect(recordFolder); + } + } + finally + { + rs.close(); + } + + return null; + } + }; + + AuthenticationUtil.runAs(runAsWork, AuthenticationUtil.getAdminUserName()); + + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomPropertyDefinitionDelete.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomPropertyDefinitionDelete.java new file mode 100644 index 0000000000..8792252392 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomPropertyDefinitionDelete.java @@ -0,0 +1,113 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminService; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONException; + +/** + * Implementation for Java backed webscript to remove RM custom property definitions + * from the custom model. + * + * @author Neil McErlean + */ +public class CustomPropertyDefinitionDelete extends AbstractRmWebScript +{ + private static final String PROP_ID = "propId"; + + private static Log logger = LogFactory.getLog(CustomPropertyDefinitionDelete.class); + + private RecordsManagementAdminService rmAdminService; + + public void setRecordsManagementAdminService(RecordsManagementAdminService rmAdminService) + { + this.rmAdminService = rmAdminService; + } + + /* + * @see org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.Status, org.alfresco.web.scripts.Cache) + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map ftlModel = null; + try + { + QName propQName = getPropertyFromReq(req); + if (logger.isDebugEnabled()) + { + StringBuilder msg = new StringBuilder(); + msg.append("Deleting property definition ").append(propQName); + logger.debug(msg.toString()); + } + ftlModel = removePropertyDefinition(propQName); + } + catch (JSONException je) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Could not parse JSON from req.", je); + } + + return ftlModel; + } + + private QName getPropertyFromReq(WebScriptRequest req) + { + Map templateVars = req.getServiceMatch().getTemplateVars(); + String propIdString = templateVars.get(PROP_ID); + + QName propQName = this.rmAdminService.getQNameForClientId(propIdString); + Map existingPropDefs = rmAdminService.getCustomPropertyDefinitions(); + + if (existingPropDefs.containsKey(propQName) == false) + { + throw new WebScriptException(HttpServletResponse.SC_NOT_FOUND, + "Requested property definition (id:" + propIdString + ") does not exist"); + } + + return propQName; + } + + /** + * Applies custom properties to the specified record node. + */ + protected Map removePropertyDefinition(QName propQName) throws JSONException + { + Map result = new HashMap(); + + rmAdminService.removeCustomPropertyDefinition(propQName); + + result.put("propertyqname", propQName.toPrefixString(namespaceService)); + + return result; + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomPropertyDefinitionPost.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomPropertyDefinitionPost.java new file mode 100644 index 0000000000..8682ad83b2 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomPropertyDefinitionPost.java @@ -0,0 +1,235 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +import java.io.IOException; +import java.io.Serializable; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminService; +import org.alfresco.module.org_alfresco_module_rm.dod5015.DOD5015Model; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementCustomModel; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.service.namespace.QName; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Implementation for Java backed webscript to add RM custom property definitions + * to the custom model. + * + * @author Neil McErlean + */ +public class CustomPropertyDefinitionPost extends BaseCustomPropertyWebScript +{ + protected RecordsManagementAdminService rmAdminService; + + private static final String PARAM_DATATYPE = "dataType"; + private static final String PARAM_TITLE = "title"; + private static final String PARAM_DESCRIPTION = "description"; + private static final String PARAM_DEFAULT_VALUE = "defaultValue"; + private static final String PARAM_MULTI_VALUED = "multiValued"; + private static final String PARAM_MANDATORY = "mandatory"; + private static final String PARAM_PROTECTED = "protected"; + private static final String PARAM_CONSTRAINT_REF = "constraintRef"; + private static final String PARAM_ELEMENT = "element"; + private static final String PARAM_LABEL = "label"; + private static final String PROP_ID = "propId"; + private static final String URL = "url"; + + public void setRecordsManagementAdminService(RecordsManagementAdminService rmAdminService) + { + this.rmAdminService = rmAdminService; + } + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + JSONObject json = null; + Map ftlModel = null; + try + { + json = new JSONObject(new JSONTokener(req.getContent().getContent())); + + ftlModel = createPropertyDefinition(req, json); + } + catch (IOException iox) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Could not read content from req.", iox); + } + catch (JSONException je) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Could not parse JSON from req.", je); + } + + return ftlModel; + } + + /** + * Applies custom properties. + */ + protected Map createPropertyDefinition(WebScriptRequest req, JSONObject json) + throws JSONException + { + Map result = new HashMap(); + Map params = getParamsFromUrlAndJson(req, json); + + QName propertyQName = createNewPropertyDefinition(params); + String localName = propertyQName.getLocalName(); + + result.put(PROP_ID, localName); + + String urlResult = req.getServicePath() + "/" + propertyQName.getLocalName(); + result.put(URL, urlResult); + + return result; + } + + @SuppressWarnings("unchecked") + protected Map getParamsFromUrlAndJson(WebScriptRequest req, JSONObject json) + throws JSONException + { + Map params; + params = new HashMap(); + params.put(PARAM_ELEMENT, req.getParameter(PARAM_ELEMENT)); + + for (Iterator iter = json.keys(); iter.hasNext(); ) + { + String nextKeyString = (String)iter.next(); + String nextValueString = json.getString(nextKeyString); + + params.put(nextKeyString, nextValueString); + } + + return params; + } + + /** + * Create a property definition based on the parameter values provided + * + * @param params parameter values + * @return {@link QName} qname of the newly created custom property + */ + protected QName createNewPropertyDefinition(Map params) + { + // Get the customisable type name + String customisableElement = (String)params.get(PARAM_ELEMENT); + QName customisableType = mapToTypeQName(customisableElement); + + String label = (String)params.get(PARAM_LABEL); + + //According to the wireframes, type here can only be date|text|number + Serializable serializableParam = params.get(PARAM_DATATYPE); + QName type = null; + if (serializableParam != null) + { + if (serializableParam instanceof String) + { + type = QName.createQName((String)serializableParam, namespaceService); + } + else if (serializableParam instanceof QName) + { + type = (QName)serializableParam; + } + else + { + throw new AlfrescoRuntimeException("Unexpected type of dataType param: "+serializableParam+" (expected String or QName)"); + } + } + + // The title is actually generated, so this parameter will be ignored + // by the RMAdminService + String title = (String)params.get(PARAM_TITLE); + String description = (String)params.get(PARAM_DESCRIPTION); + String defaultValue = (String)params.get(PARAM_DEFAULT_VALUE); + + boolean mandatory = false; + serializableParam = params.get(PARAM_MANDATORY); + if (serializableParam != null) + { + mandatory = Boolean.valueOf(serializableParam.toString()); + } + + boolean isProtected = false; + serializableParam = params.get(PARAM_PROTECTED); + if (serializableParam != null) + { + isProtected = Boolean.valueOf(serializableParam.toString()); + } + + boolean multiValued = false; + serializableParam = params.get(PARAM_MULTI_VALUED); + if (serializableParam != null) + { + multiValued = Boolean.valueOf(serializableParam.toString()); + } + + serializableParam = params.get(PARAM_CONSTRAINT_REF); + QName constraintRef = null; + if (serializableParam != null) + { + if (serializableParam instanceof String) + { + constraintRef = QName.createQName((String)serializableParam, namespaceService); + } + else if (serializableParam instanceof QName) + { + constraintRef = (QName)serializableParam; + } + else + { + throw new AlfrescoRuntimeException("Unexpected type of constraintRef param: "+serializableParam+" (expected String or QName)"); + } + } + + // if propId is specified, use it. + QName proposedQName = null; + String propId = (String)params.get(PROP_ID); + if (propId != null) + { + proposedQName = QName.createQName(RecordsManagementCustomModel.RM_CUSTOM_PREFIX, propId, namespaceService); + } + + return rmAdminService.addCustomPropertyDefinition( + proposedQName, + customisableType, + label, + type, + title, + description, + defaultValue, + multiValued, + mandatory, + isProtected, + constraintRef); + } + + + +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomPropertyDefinitionPut.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomPropertyDefinitionPut.java new file mode 100644 index 0000000000..fd4ec16c89 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomPropertyDefinitionPut.java @@ -0,0 +1,178 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +import java.io.IOException; +import java.io.Serializable; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminService; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.surf.util.ParameterCheck; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; + +/** + * Implementation for Java backed webscript to update RM custom property definitions + * in the custom model. + * + * @author Neil McErlean + */ +public class CustomPropertyDefinitionPut extends BaseCustomPropertyWebScript +{ + private RecordsManagementAdminService rmAdminService; + + private static final String PARAM_LABEL = "label"; + private static final String PARAM_CONSTRAINT_REF = "constraintRef"; + private static final String PROP_ID = "propId"; + private static final String URL = "url"; + + public void setRecordsManagementAdminService(RecordsManagementAdminService rmAdminService) + { + this.rmAdminService = rmAdminService; + } + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + JSONObject json = null; + Map ftlModel = null; + try + { + json = new JSONObject(new JSONTokener(req.getContent().getContent())); + + ftlModel = handlePropertyDefinitionUpdate(req, json); + } + catch (IOException iox) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Could not read content from req.", iox); + } + catch (JSONException je) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Could not parse JSON from req.", je); + } + + return ftlModel; + } + + /** + * Applies custom properties. + */ + protected Map handlePropertyDefinitionUpdate(WebScriptRequest req, JSONObject json) + throws JSONException + { + Map result = new HashMap(); + + Map params = getParamsFromUrlAndJson(req, json); + + QName propertyQName; + propertyQName = updatePropertyDefinition(params); + String localName = propertyQName.getLocalName(); + + result.put(PROP_ID, localName); + + String urlResult = req.getServicePath(); + result.put(URL, urlResult); + + return result; + } + + /** + * If label has a non-null value, it is set on the property def. + * If constraintRef has a non-null value, it is set on this propDef. + * If constraintRef has a null value, all constraints for that propDef are removed. + * + * @param params + * @return + */ + protected QName updatePropertyDefinition(Map params) + { + QName result = null; + + String propId = (String)params.get(PROP_ID); + ParameterCheck.mandatoryString("propId", propId); + + QName propQName = rmAdminService.getQNameForClientId(propId); + if (propQName == null) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, + "Could not find property definition for: " + propId); + } + + if (params.containsKey(PARAM_LABEL)) + { + String label = (String)params.get(PARAM_LABEL); + result = rmAdminService.setCustomPropertyDefinitionLabel(propQName, label); + } + + if (params.containsKey(PARAM_CONSTRAINT_REF)) + { + String constraintRef = (String)params.get(PARAM_CONSTRAINT_REF); + + if (constraintRef == null) + { + result = rmAdminService.removeCustomPropertyDefinitionConstraints(propQName); + } + else + { + QName constraintRefQName = QName.createQName(constraintRef, namespaceService); + result = rmAdminService.setCustomPropertyDefinitionConstraint(propQName, constraintRefQName); + } + } + return result; + } + + @SuppressWarnings("unchecked") + protected Map getParamsFromUrlAndJson(WebScriptRequest req, JSONObject json) + throws JSONException + { + Map params; + params = new HashMap(); + + Map templateVars = req.getServiceMatch().getTemplateVars(); + String propId = templateVars.get(PROP_ID); + if (propId != null) + { + params.put(PROP_ID, (Serializable)propId); + } + + for (Iterator iter = json.keys(); iter.hasNext(); ) + { + String nextKeyString = (String)iter.next(); + String nextValueString = null; + if (!json.isNull(nextKeyString)) + { + nextValueString = json.getString(nextKeyString); + } + + params.put(nextKeyString, nextValueString); + } + + return params; + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomPropertyDefinitionsGet.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomPropertyDefinitionsGet.java new file mode 100644 index 0000000000..0a0d06ea69 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomPropertyDefinitionsGet.java @@ -0,0 +1,118 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminService; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.namespace.QName; +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.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class provides the implementation for the custompropdefinitions.get webscript. + * + * @author Neil McErlean + */ +public class CustomPropertyDefinitionsGet extends BaseCustomPropertyWebScript +{ + /** Logger */ + private static Log logger = LogFactory.getLog(CustomPropertyDefinitionsGet.class); + + private static final String ELEMENT = "element"; + private static final String PROP_ID = "propId"; + + /** Records management admin service */ + private RecordsManagementAdminService rmAdminService; + + /** + * @param rmAdminService records management admin service + */ + public void setRecordsManagementAdminService(RecordsManagementAdminService rmAdminService) + { + this.rmAdminService = rmAdminService; + } + + /** + * @see org.springframework.extensions.webscripts.DeclarativeWebScript#executeImpl(org.springframework.extensions.webscripts.WebScriptRequest, org.springframework.extensions.webscripts.Status, org.springframework.extensions.webscripts.Cache) + */ + @Override + public Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(); + + Map templateVars = req.getServiceMatch().getTemplateVars(); + String propId = templateVars.get(PROP_ID); + String elementName = req.getParameter(ELEMENT); + + if (logger.isDebugEnabled() && elementName != null) + { + logger.debug("Getting custom property definitions for elementName " + elementName); + } + else if (logger.isDebugEnabled() && propId != null) + { + logger.debug("Getting custom property definition for propId " + propId); + } + + // If propId has been provided then this is a request for a single custom-property-defn. + // else it is a request for all defined on the specified element. + List propData = new ArrayList(); + if (propId != null) + { + QName propQName = rmAdminService.getQNameForClientId(propId); + PropertyDefinition propDefn = rmAdminService.getCustomPropertyDefinitions().get(propQName); + if (propQName == null || propDefn == null) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "Property definition for " + propId + " not found."); + } + propData.add(propDefn); + } + else if (elementName != null) + { + QName customisableType = mapToTypeQName(elementName); + Map currentCustomProps = rmAdminService.getCustomPropertyDefinitions(customisableType); + for (Entry entry : currentCustomProps.entrySet()) + { + propData.add(entry.getValue()); + } + } + else + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Either elementName or propId must be specified."); + } + + if (logger.isDebugEnabled()) + { + logger.debug("Retrieved custom property definitions: " + propData); + } + + model.put("customProps", propData); + + return model; + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomRefDelete.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomRefDelete.java new file mode 100644 index 0000000000..bc84bb82ce --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomRefDelete.java @@ -0,0 +1,112 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Implementation for Java backed webscript to remove RM custom reference instances + * from a node. + * + * @author Neil McErlean + */ +public class CustomRefDelete extends AbstractRmWebScript +{ + private static Log logger = LogFactory.getLog(CustomRefDelete.class); + + private RecordsManagementAdminService rmAdminService; + + public void setRecordsManagementAdminService(RecordsManagementAdminService rmAdminService) + { + this.rmAdminService = rmAdminService; + } + + /* + * @see org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.Status, org.alfresco.web.scripts.Cache) + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map ftlModel = removeCustomReferenceInstance(req); + + return ftlModel; + } + + /** + * Removes custom reference. + */ + protected Map removeCustomReferenceInstance(WebScriptRequest req) + { + NodeRef fromNodeRef = parseRequestForNodeRef(req); + + // Get the toNode from the URL query string. + String storeType = req.getParameter("st"); + String storeId = req.getParameter("si"); + String nodeId = req.getParameter("id"); + + // create the NodeRef and ensure it is valid + StoreRef storeRef = new StoreRef(storeType, storeId); + NodeRef toNodeRef = new NodeRef(storeRef, nodeId); + + if (!this.nodeService.exists(toNodeRef)) + { + throw new WebScriptException(HttpServletResponse.SC_NOT_FOUND, "Unable to find to-node: " + + toNodeRef.toString()); + } + + Map result = new HashMap(); + + Map templateVars = req.getServiceMatch().getTemplateVars(); + String clientsRefId = templateVars.get("refId"); + QName qn = rmAdminService.getQNameForClientId(clientsRefId); + if (qn == null) + { + throw new WebScriptException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, + "Unable to find reference type: " + clientsRefId); + } + + if (logger.isDebugEnabled()) + { + StringBuilder msg = new StringBuilder(); + msg.append("Removing reference ").append(qn).append(" from ") + .append(fromNodeRef).append(" to ").append(toNodeRef); + logger.debug(msg.toString()); + } + + rmAdminService.removeCustomReference(fromNodeRef, toNodeRef, qn); + + result.put("success", true); + + return result; + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomRefPost.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomRefPost.java new file mode 100644 index 0000000000..053f98ee43 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomRefPost.java @@ -0,0 +1,115 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; + +/** + * Implementation for Java backed webscript to add RM custom reference instances + * to a node. + * + * @author Neil McErlean + */ +public class CustomRefPost extends AbstractRmWebScript +{ + private static final String TO_NODE = "toNode"; + private static final String REF_ID = "refId"; + + @SuppressWarnings("unused") + private static Log logger = LogFactory.getLog(CustomRefPost.class); + + private RecordsManagementAdminService rmAdminService; + + public void setRecordsManagementAdminService(RecordsManagementAdminService rmAdminService) + { + this.rmAdminService = rmAdminService; + } + + /* + * @see org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.Status, org.alfresco.web.scripts.Cache) + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + JSONObject json = null; + Map ftlModel = null; + try + { + json = new JSONObject(new JSONTokener(req.getContent().getContent())); + + ftlModel = addCustomReferenceInstance(req, json); + } + catch (IOException iox) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Could not read content from req.", iox); + } + catch (JSONException je) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Could not parse JSON from req.", je); + } + + return ftlModel; + } + + /** + * Applies custom reference. + */ + protected Map addCustomReferenceInstance(WebScriptRequest req, JSONObject json) throws JSONException + { + NodeRef fromNode = parseRequestForNodeRef(req); + + Map result = new HashMap(); + + String toNodeStg = json.getString(TO_NODE); + NodeRef toNode = new NodeRef(toNodeStg); + + String clientsRefId = json.getString(REF_ID); + QName qn = rmAdminService.getQNameForClientId(clientsRefId); + if (qn == null) + { + throw new WebScriptException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, + "Unable to find reference type: " + clientsRefId); + } + + rmAdminService.addCustomReference(fromNode, toNode, qn); + + result.put("success", true); + + return result; + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomReferenceDefinitionPost.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomReferenceDefinitionPost.java new file mode 100644 index 0000000000..efe3492ab1 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomReferenceDefinitionPost.java @@ -0,0 +1,174 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +import java.io.IOException; +import java.io.Serializable; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminService; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.surf.util.ParameterCheck; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; + +/** + * Implementation for Java backed webscript to add RM custom reference definitions + * to the custom model. + * + * @author Neil McErlean + */ +public class CustomReferenceDefinitionPost extends AbstractRmWebScript +{ + private static final String URL = "url"; + private static final String REF_ID = "refId"; + private static final String TARGET = "target"; + private static final String SOURCE = "source"; + private static final String LABEL = "label"; + private static final String REFERENCE_TYPE = "referenceType"; + + private static Log logger = LogFactory.getLog(CustomReferenceDefinitionPost.class); + + private RecordsManagementAdminService rmAdminService; + + public void setRecordsManagementAdminService(RecordsManagementAdminService rmAdminService) + { + this.rmAdminService = rmAdminService; + } + + /* + * @see org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.Status, org.alfresco.web.scripts.Cache) + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + JSONObject json = null; + Map ftlModel = null; + try + { + json = new JSONObject(new JSONTokener(req.getContent().getContent())); + + ftlModel = addCustomReference(req, json); + } + catch (IOException iox) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Could not read content from req.", iox); + } + catch (JSONException je) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Could not parse JSON from req.", je); + } + + return ftlModel; + } + + /** + * Applies custom properties. + */ + @SuppressWarnings("unchecked") + protected Map addCustomReference(WebScriptRequest req, JSONObject json) throws JSONException + { + Map result = new HashMap(); + Map params = new HashMap(); + + for (Iterator iter = json.keys(); iter.hasNext(); ) + { + String nextKeyString = (String)iter.next(); + Serializable nextValue = (Serializable)json.get(nextKeyString); + + params.put(nextKeyString, nextValue); + } + String refTypeParam = (String)params.get(REFERENCE_TYPE); + ParameterCheck.mandatory(REFERENCE_TYPE, refTypeParam); + CustomReferenceType refTypeEnum = CustomReferenceType.getEnumFromString(refTypeParam); + + boolean isChildAssoc = refTypeEnum.equals(CustomReferenceType.PARENT_CHILD); + + if (logger.isDebugEnabled()) + { + StringBuilder msg = new StringBuilder(); + msg.append("Creating custom "); + if (isChildAssoc) + { + msg.append("child "); + } + msg.append("assoc"); + logger.debug(msg.toString()); + } + + QName generatedQName; + if (isChildAssoc) + { + String source = (String)params.get(SOURCE); + String target = (String)params.get(TARGET); + + generatedQName = rmAdminService.addCustomChildAssocDefinition(source, target); + } + else + { + String label = (String)params.get(LABEL); + + generatedQName = rmAdminService.addCustomAssocDefinition(label); + } + + result.put(REFERENCE_TYPE, refTypeParam); + + String qnameLocalName; + if (refTypeParam.equals(CustomReferenceType.BIDIRECTIONAL.toString())) + { + Serializable labelParam = params.get(LABEL); + // label is mandatory for bidirectional refs only + ParameterCheck.mandatory(LABEL, labelParam); + + qnameLocalName = generatedQName.getLocalName(); + result.put(REF_ID, qnameLocalName); + } + else if (refTypeParam.equals(CustomReferenceType.PARENT_CHILD.toString())) + { + Serializable sourceParam = params.get(SOURCE); + Serializable targetParam = params.get(TARGET); + // source,target mandatory for parent/child refs only + ParameterCheck.mandatory(SOURCE, sourceParam); + ParameterCheck.mandatory(TARGET, targetParam); + + qnameLocalName = generatedQName.getLocalName(); + result.put(REF_ID, qnameLocalName); + } + else + { + throw new WebScriptException("Unsupported reference type: " + refTypeParam); + } + result.put(URL, req.getServicePath() + "/" + qnameLocalName); + + result.put("success", Boolean.TRUE); + + return result; + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomReferenceDefinitionPut.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomReferenceDefinitionPut.java new file mode 100644 index 0000000000..02186dde62 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomReferenceDefinitionPut.java @@ -0,0 +1,153 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +import java.io.IOException; +import java.io.Serializable; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminService; +import org.alfresco.service.cmr.dictionary.AssociationDefinition; +import org.alfresco.service.cmr.dictionary.ChildAssociationDefinition; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; + +/** + * Implementation for Java backed webscript to update RM custom reference definitions. + * There is currently only support for updating the label (for bidirectional references) or + * the source/target (for parent/child references). + * + * @author Neil McErlean + */ +public class CustomReferenceDefinitionPut extends AbstractRmWebScript +{ + private static final String URL = "url"; + private static final String REF_ID = "refId"; + private static final String TARGET = "target"; + private static final String SOURCE = "source"; + private static final String LABEL = "label"; + + private static Log logger = LogFactory.getLog(CustomReferenceDefinitionPut.class); + + private RecordsManagementAdminService rmAdminService; + + public void setRecordsManagementAdminService(RecordsManagementAdminService rmAdminService) + { + this.rmAdminService = rmAdminService; + } + + /* + * @see org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.Status, org.alfresco.web.scripts.Cache) + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + JSONObject json = null; + Map ftlModel = null; + try + { + json = new JSONObject(new JSONTokener(req.getContent().getContent())); + + ftlModel = updateCustomReference(req, json); + } + catch (IOException iox) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Could not read content from req.", iox); + } + catch (JSONException je) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Could not parse JSON from req.", je); + } + + return ftlModel; + } + + /** + * Applies custom properties. + */ + @SuppressWarnings("unchecked") + protected Map updateCustomReference(WebScriptRequest req, JSONObject json) throws JSONException + { + Map result = new HashMap(); + Map params = new HashMap(); + + for (Iterator iter = json.keys(); iter.hasNext(); ) + { + String nextKeyString = (String)iter.next(); + Serializable nextValue = (Serializable)json.get(nextKeyString); + + params.put(nextKeyString, nextValue); + } + + Map templateVars = req.getServiceMatch().getTemplateVars(); + String refId = templateVars.get(REF_ID); + // refId cannot be null as it is defined within the URL + params.put(REF_ID, (Serializable)refId); + + // Ensure that the reference actually exists. + QName refQName = rmAdminService.getQNameForClientId(refId); + if (refQName == null) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, + "Could not find reference definition for: " + refId); + } + + String newLabel = (String)params.get(LABEL); + String newSource = (String)params.get(SOURCE); + String newTarget = (String)params.get(TARGET); + + // Determine whether it's a bidi or a p/c ref + AssociationDefinition assocDef = rmAdminService.getCustomReferenceDefinitions().get(refQName); + if (assocDef == null) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, + "Could not find reference definition for: " + refId); + } + + if (assocDef instanceof ChildAssociationDefinition) + { + if (newSource != null || newTarget != null) + { + rmAdminService.updateCustomChildAssocDefinition(refQName, newSource, newTarget); + } + } + else if (newLabel != null) + { + rmAdminService.updateCustomAssocDefinition(refQName, newLabel); + } + + result.put(URL, req.getServicePath()); + result.put("refId", refQName.getLocalName()); + result.put("success", Boolean.TRUE); + + return result; + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomReferenceDefinitionsGet.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomReferenceDefinitionsGet.java new file mode 100644 index 0000000000..208102feba --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomReferenceDefinitionsGet.java @@ -0,0 +1,159 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminService; +import org.alfresco.service.cmr.dictionary.AssociationDefinition; +import org.alfresco.service.cmr.dictionary.ChildAssociationDefinition; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.webscripts.Cache; +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.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * This class provides the implementation for the customrefdefinitions.get webscript. + * + * @author Neil McErlean + */ +public class CustomReferenceDefinitionsGet extends DeclarativeWebScript +{ + private static final String REFERENCE_TYPE = "referenceType"; + private static final String REF_ID = "refId"; + private static final String LABEL = "label"; + private static final String SOURCE = "source"; + private static final String TARGET = "target"; + private static final String CUSTOM_REFS = "customRefs"; + private static Log logger = LogFactory.getLog(CustomReferenceDefinitionsGet.class); + + private RecordsManagementAdminService rmAdminService; + private NamespaceService namespaceService; + + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + public void setRecordsManagementAdminService(RecordsManagementAdminService rmAdminService) + { + this.rmAdminService = rmAdminService; + } + + @Override + public Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(); + + Map templateVars = req.getServiceMatch().getTemplateVars(); + String refId = templateVars.get(REF_ID); + + + if (logger.isDebugEnabled()) + { + logger.debug("Getting custom reference definitions with refId: " + String.valueOf(refId)); + } + + Map currentCustomRefs = rmAdminService.getCustomReferenceDefinitions(); + + // If refId has been provided then this is a request for a single custom-ref-defn. + // else it is a request for them all. + if (refId != null) + { + QName qn = rmAdminService.getQNameForClientId(refId); + + AssociationDefinition assDef = currentCustomRefs.get(qn); + if (assDef == null) + { + StringBuilder msg = new StringBuilder(); + msg.append("Unable to find reference: ").append(refId); + if (logger.isDebugEnabled()) + { + logger.debug(msg.toString()); + } + throw new WebScriptException(HttpServletResponse.SC_NOT_FOUND, + msg.toString()); + } + + currentCustomRefs = new HashMap(1); + currentCustomRefs.put(qn, assDef); + } + + List> listOfReferenceData = new ArrayList>(); + + for (Entry entry : currentCustomRefs.entrySet()) + { + Map data = new HashMap(); + + AssociationDefinition nextValue = entry.getValue(); + + CustomReferenceType referenceType = nextValue instanceof ChildAssociationDefinition ? + CustomReferenceType.PARENT_CHILD : CustomReferenceType.BIDIRECTIONAL; + + data.put(REFERENCE_TYPE, referenceType.toString()); + + // It is the title which stores either the label, or the source and target. + String nextTitle = nextValue.getTitle(); + if (CustomReferenceType.PARENT_CHILD.equals(referenceType)) + { + if (nextTitle != null) + { + String[] sourceAndTarget = rmAdminService.splitSourceTargetId(nextTitle); + data.put(SOURCE, sourceAndTarget[0]); + data.put(TARGET, sourceAndTarget[1]); + data.put(REF_ID, entry.getKey().getLocalName()); + } + } + else if (CustomReferenceType.BIDIRECTIONAL.equals(referenceType)) + { + if (nextTitle != null) + { + data.put(LABEL, nextTitle); + data.put(REF_ID, entry.getKey().getLocalName()); + } + } + else + { + throw new WebScriptException("Unsupported custom reference type: " + referenceType); + } + + listOfReferenceData.add(data); + } + + if (logger.isDebugEnabled()) + { + logger.debug("Retrieved custom reference definitions: " + listOfReferenceData.size()); + } + + model.put(CUSTOM_REFS, listOfReferenceData); + + return model; + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomReferenceType.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomReferenceType.java new file mode 100644 index 0000000000..b395aff4ca --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomReferenceType.java @@ -0,0 +1,55 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +/** + * This enum represents the allowed types of custom references. + * + * @author Neil McErlean + */ +public enum CustomReferenceType +{ + PARENT_CHILD("parentchild"), + BIDIRECTIONAL("bidirectional"); + + private final String printableString; + + private CustomReferenceType(String printableString) + { + this.printableString = printableString; + } + + @Override + public String toString() + { + return this.printableString; + } + + public static CustomReferenceType getEnumFromString(String stg) + { + for (CustomReferenceType type : CustomReferenceType.values()) + { + if (type.printableString.equals(stg)) + { + return type; + } + } + throw new IllegalArgumentException("Unrecognised CustomReferenceType: " + stg); + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomRefsGet.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomRefsGet.java new file mode 100644 index 0000000000..4a2dfbe997 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomRefsGet.java @@ -0,0 +1,179 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminService; +import org.alfresco.service.cmr.dictionary.AssociationDefinition; +import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * This class provides the implementation for the customrefs.get webscript. + * + * @author Neil McErlean + */ +public class CustomRefsGet extends AbstractRmWebScript +{ + private static final String REFERENCE_TYPE = "referenceType"; + private static final String REF_ID = "refId"; + private static final String LABEL = "label"; + private static final String SOURCE = "source"; + private static final String TARGET = "target"; + private static final String PARENT_REF = "parentRef"; + private static final String CHILD_REF = "childRef"; + private static final String SOURCE_REF = "sourceRef"; + private static final String TARGET_REF = "targetRef"; + private static final String CUSTOM_REFS_FROM = "customRefsFrom"; + private static final String CUSTOM_REFS_TO = "customRefsTo"; + private static final String NODE_NAME = "nodeName"; + private static final String NODE_TITLE = "nodeTitle"; + + private static Log logger = LogFactory.getLog(CustomRefsGet.class); + private RecordsManagementAdminService rmAdminService; + + public void setRecordsManagementAdminService(RecordsManagementAdminService rmAdminService) + { + this.rmAdminService = rmAdminService; + } + + @Override + public Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map ftlModel = new HashMap(); + + NodeRef node = parseRequestForNodeRef(req); + + if (logger.isDebugEnabled()) + { + logger.debug("Getting custom reference instances for " + node); + } + + // All the references that come 'out' from this node. + List> listOfOutwardReferenceData = new ArrayList>(); + + List assocsFromThisNode = this.rmAdminService.getCustomReferencesFrom(node); + addBidirectionalReferenceData(listOfOutwardReferenceData, assocsFromThisNode); + + List childAssocs = this.rmAdminService.getCustomChildReferences(node); + addParentChildReferenceData(listOfOutwardReferenceData, childAssocs); + + // All the references that come 'in' to this node. + List> listOfInwardReferenceData = new ArrayList>(); + + List toAssocs = this.rmAdminService.getCustomReferencesTo(node); + addBidirectionalReferenceData(listOfInwardReferenceData, toAssocs); + + List parentAssocs = this.rmAdminService.getCustomParentReferences(node); + addParentChildReferenceData(listOfInwardReferenceData, parentAssocs); + + if (logger.isDebugEnabled()) + { + logger.debug("Retrieved custom reference instances: " + assocsFromThisNode); + } + + ftlModel.put(NODE_NAME, nodeService.getProperty(node, ContentModel.PROP_NAME)); + ftlModel.put(NODE_TITLE, nodeService.getProperty(node, ContentModel.PROP_TITLE)); + ftlModel.put(CUSTOM_REFS_FROM, listOfOutwardReferenceData); + ftlModel.put(CUSTOM_REFS_TO, listOfInwardReferenceData); + + return ftlModel; + } + + /** + * This method goes through the associationRefs specified and constructs a Map + * for each assRef. FTL-relevant data are added to that map. The associationRefs must all be + * parent/child references. + * + * @param listOfReferenceData + * @param assocs + */ + private void addParentChildReferenceData(List> listOfReferenceData, + List childAssocs) + { + for (ChildAssociationRef childAssRef : childAssocs) + { + Map data = new HashMap(); + + QName typeQName = childAssRef.getTypeQName(); + + data.put(CHILD_REF, childAssRef.getChildRef().toString()); + data.put(PARENT_REF, childAssRef.getParentRef().toString()); + + AssociationDefinition assDef = rmAdminService.getCustomReferenceDefinitions().get(typeQName); + + if (assDef != null) + { + String compoundTitle = assDef.getTitle(); + + data.put(REF_ID, typeQName.getLocalName()); + + String[] sourceAndTarget = rmAdminService.splitSourceTargetId(compoundTitle); + data.put(SOURCE, sourceAndTarget[0]); + data.put(TARGET, sourceAndTarget[1]); + data.put(REFERENCE_TYPE, CustomReferenceType.PARENT_CHILD.toString()); + + listOfReferenceData.add(data); + } + } + } + + /** + * This method goes through the associationRefs specified and constructs a Map + * for each assRef. FTL-relevant data are added to that map. The associationRefs must all be + * bidirectional references. + * + * @param listOfReferenceData + * @param assocs + */ + private void addBidirectionalReferenceData(List> listOfReferenceData, + List assocs) + { + for (AssociationRef assRef : assocs) + { + Map data = new HashMap(); + + QName typeQName = assRef.getTypeQName(); + AssociationDefinition assDef = rmAdminService.getCustomReferenceDefinitions().get(typeQName); + + if (assDef != null) + { + data.put(LABEL, assDef.getTitle()); + data.put(REF_ID, typeQName.getLocalName()); + data.put(REFERENCE_TYPE, CustomReferenceType.BIDIRECTIONAL.toString()); + data.put(SOURCE_REF, assRef.getSourceRef().toString()); + data.put(TARGET_REF, assRef.getTargetRef().toString()); + + listOfReferenceData.add(data); + } + } + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomisableGet.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomisableGet.java new file mode 100644 index 0000000000..ea9d52b5a8 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomisableGet.java @@ -0,0 +1,174 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminService; +import org.alfresco.service.cmr.dictionary.ClassDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class provides the implementation for the customisable.get webscript. + * + * @author Roy Wetherall + */ +public class CustomisableGet extends DeclarativeWebScript +{ + /** Logger */ + @SuppressWarnings("unused") + private static Log logger = LogFactory.getLog(CustomisableGet.class); + + /** Records management admin service */ + private RecordsManagementAdminService rmAdminService; + + /** Dictionary service */ + private DictionaryService dictionaryService; + + /** Namespace service */ + private NamespaceService namespaceService; + + /** + * @param rmAdminService records management admin service + */ + public void setRecordsManagementAdminService(RecordsManagementAdminService rmAdminService) + { + this.rmAdminService = rmAdminService; + } + + /** + * @param namespaceService namespace service + */ + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + /** + * @param dictionaryService dictionary service + */ + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + /** + * @see org.springframework.extensions.webscripts.DeclarativeWebScript#executeImpl(org.springframework.extensions.webscripts.WebScriptRequest, org.springframework.extensions.webscripts.Status, org.springframework.extensions.webscripts.Cache) + */ + @Override + public Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(); + + Set qnames = rmAdminService.getCustomisable(); + ArrayList items = new ArrayList(qnames.size()); + for (QName qname : qnames) + { + ClassDefinition definition = dictionaryService.getClass(qname); + if (definition != null) + { + String name = qname.toPrefixString(namespaceService); + String title = definition.getTitle(); + if (title == null || title.length() == 0) + { + title = qname.getLocalName(); + } + boolean isAspect = definition.isAspect(); + + items.add(new Item(name, isAspect, title)); + } + } + + // Sort the customisable types and aspects by title + Collections.sort(items, new Comparator() + { + @Override + public int compare(Item o1, Item o2) + { + return o1.title.compareToIgnoreCase(o2.title); + }}); + + model.put("items", items); + return model; + } + + /** + * Model items + */ + public class Item + { + private String name; + private boolean isAspect; + private String title; + + public Item(String name, boolean isAspect, String title) + { + this.name = name; + this.isAspect = isAspect; + this.title = title; + } + + public String getName() + { + return name; + } + + public boolean getIsAspect() + { + return isAspect; + } + + public String getTitle() + { + return title; + } + + @Override + public int hashCode() + { + int var_code = (null == name ? 0 : name.hashCode()); + return 31 + var_code; + } + + @Override + public boolean equals(Object obj) + { + if (obj == null || (obj.getClass() != this.getClass())) + { + return false; + } + else + { + return this.name.equals(((Item)obj).name); + } + } + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/DispositionAbstractBase.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/DispositionAbstractBase.java new file mode 100644 index 0000000000..e7bca4c641 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/DispositionAbstractBase.java @@ -0,0 +1,155 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionActionDefinition; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule; +import org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEvent; +import org.alfresco.service.cmr.repository.NodeRef; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Abstract base class for all disposition related java backed webscripts. + * + * @author Gavin Cornwell + */ +public class DispositionAbstractBase extends AbstractRmWebScript +{ + /** + * Parses the request and providing it's valid returns the DispositionSchedule object. + * + * @param req The webscript request + * @return The DispositionSchedule object the request is aimed at + */ + protected DispositionSchedule parseRequestForSchedule(WebScriptRequest req) + { + // get the NodeRef from the request + NodeRef nodeRef = parseRequestForNodeRef(req); + + // Determine whether we are getting the inherited disposition schedule or not + boolean inherited = true; + String inheritedString = req.getParameter("inherited"); + if (inheritedString != null) + { + inherited = Boolean.parseBoolean(inheritedString); + } + + // make sure the node passed in has a disposition schedule attached + DispositionSchedule schedule = null; + if (inherited == true) + { + schedule = this.dispositionService.getDispositionSchedule(nodeRef); + } + else + { + schedule = dispositionService.getAssociatedDispositionSchedule(nodeRef); + } + if (schedule == null) + { + throw new WebScriptException(HttpServletResponse.SC_NOT_FOUND, "Node " + + nodeRef.toString() + " does not have a disposition schedule"); + } + + return schedule; + } + + /** + * Parses the request and providing it's valid returns the DispositionActionDefinition object. + * + * @param req The webscript request + * @param schedule The disposition schedule + * @return The DispositionActionDefinition object the request is aimed at + */ + protected DispositionActionDefinition parseRequestForActionDefinition(WebScriptRequest req, + DispositionSchedule schedule) + { + // make sure the requested action definition exists + Map templateVars = req.getServiceMatch().getTemplateVars(); + String actionDefId = templateVars.get("action_def_id"); + DispositionActionDefinition actionDef = schedule.getDispositionActionDefinition(actionDefId); + if (actionDef == null) + { + throw new WebScriptException(HttpServletResponse.SC_NOT_FOUND, + "Requested disposition action definition (id:" + actionDefId + ") does not exist"); + } + + return actionDef; + } + + /** + * Helper to create a model to represent the given disposition action definition. + * + * @param actionDef The DispositionActionDefinition instance to generate model for + * @param url The URL for the DispositionActionDefinition + * @return Map representing the model + */ + protected Map createActionDefModel(DispositionActionDefinition actionDef, + String url) + { + Map model = new HashMap(8); + + model.put("id", actionDef.getId()); + model.put("index", actionDef.getIndex()); + model.put("url", url); + model.put("name", actionDef.getName()); + model.put("label", actionDef.getLabel()); + model.put("eligibleOnFirstCompleteEvent", actionDef.eligibleOnFirstCompleteEvent()); + + if (actionDef.getDescription() != null) + { + model.put("description", actionDef.getDescription()); + } + + if (actionDef.getPeriod() != null) + { + model.put("period", actionDef.getPeriod().toString()); + } + + if (actionDef.getPeriodProperty() != null) + { + model.put("periodProperty", actionDef.getPeriodProperty().toPrefixString(this.namespaceService)); + } + + if (actionDef.getLocation() != null) + { + model.put("location", actionDef.getLocation()); + } + + List events = actionDef.getEvents(); + if (events != null && events.size() > 0) + { + List eventNames = new ArrayList(events.size()); + for (RecordsManagementEvent event : events) + { + eventNames.add(event.getName()); + } + model.put("events", eventNames); + } + + return model; + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/DispositionActionDefinitionDelete.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/DispositionActionDefinitionDelete.java new file mode 100644 index 0000000000..453219b878 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/DispositionActionDefinitionDelete.java @@ -0,0 +1,55 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionActionDefinition; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Implementation for Java backed webscript to delete a dispostion action definition. + * + * @author Gavin Cornwell + */ +public class DispositionActionDefinitionDelete extends DispositionAbstractBase +{ + /* + * @see org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.Status, org.alfresco.web.scripts.Cache) + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + // parse the request to retrieve the schedule object + DispositionSchedule schedule = parseRequestForSchedule(req); + + // parse the request to retrieve the action definition object + DispositionActionDefinition actionDef = parseRequestForActionDefinition(req, schedule); + + // remove the action definition from the schedule + this.dispositionService.removeDispositionActionDefinition(schedule, actionDef); + + // return an empty model + return new HashMap(); + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/DispositionActionDefinitionPost.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/DispositionActionDefinitionPost.java new file mode 100644 index 0000000000..4ec827165c --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/DispositionActionDefinitionPost.java @@ -0,0 +1,147 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +import java.io.IOException; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionActionDefinition; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; + +/** + * Implementation for Java backed webscript to create a new dispositon action definition. + * + * @author Gavin Cornwell + */ +public class DispositionActionDefinitionPost extends DispositionAbstractBase +{ + /* + * @see org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.Status, org.alfresco.web.scripts.Cache) + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + // parse the request to retrieve the schedule object + DispositionSchedule schedule = parseRequestForSchedule(req); + + // retrieve the rest of the post body and create the action + // definition + JSONObject json = null; + DispositionActionDefinition actionDef = null; + try + { + json = new JSONObject(new JSONTokener(req.getContent().getContent())); + actionDef = createActionDefinition(json, schedule); + } + catch (IOException iox) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Could not read content from req.", iox); + } + catch (JSONException je) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Could not parse JSON from req.", je); + } + + // create model object with just the action data + Map model = new HashMap(1); + model.put("action", createActionDefModel(actionDef, req.getURL() + "/" + actionDef.getId())); + return model; + } + + /** + * Creates a dispositionActionDefinition node in the repo. + * + * @param json The JSON to use to create the action definition + * @param schedule The DispositionSchedule the action is for + * @return The DispositionActionDefinition representing the new action definition + */ + protected DispositionActionDefinition createActionDefinition(JSONObject json, + DispositionSchedule schedule) throws JSONException + { + // extract the data from the JSON request + if (json.has("name") == false) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Mandatory 'name' parameter was not provided in request body"); + } + + // create the properties for the action definition + Map props = new HashMap(8); + String name = json.getString("name"); + props.put(RecordsManagementModel.PROP_DISPOSITION_ACTION_NAME, name); + + if (json.has("description")) + { + props.put(RecordsManagementModel.PROP_DISPOSITION_DESCRIPTION, json.getString("description")); + } + + if (json.has("period")) + { + props.put(RecordsManagementModel.PROP_DISPOSITION_PERIOD, json.getString("period")); + } + + if (json.has("periodProperty")) + { + QName periodProperty = QName.createQName(json.getString("periodProperty"), this.namespaceService); + props.put(RecordsManagementModel.PROP_DISPOSITION_PERIOD_PROPERTY, periodProperty); + } + + if (json.has("eligibleOnFirstCompleteEvent")) + { + props.put(RecordsManagementModel.PROP_DISPOSITION_EVENT_COMBINATION, + json.getBoolean("eligibleOnFirstCompleteEvent") ? "or" : "and"); + } + + if (json.has("location")) + { + props.put(RecordsManagementModel.PROP_DISPOSITION_LOCATION, + json.getString("location")); + } + + if (json.has("events")) + { + JSONArray events = json.getJSONArray("events"); + List eventsList = new ArrayList(events.length()); + for (int x = 0; x < events.length(); x++) + { + eventsList.add(events.getString(x)); + } + props.put(RecordsManagementModel.PROP_DISPOSITION_EVENT, (Serializable)eventsList); + } + + // add the action definition to the schedule + return this.dispositionService.addDispositionActionDefinition(schedule, props); + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/DispositionActionDefinitionPut.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/DispositionActionDefinitionPut.java new file mode 100644 index 0000000000..1a1b112f38 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/DispositionActionDefinitionPut.java @@ -0,0 +1,145 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +import java.io.IOException; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionActionDefinition; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; + +/** + * Implementation for Java backed webscript to update an existing dispositon action definition. + * + * @author Gavin Cornwell + */ +public class DispositionActionDefinitionPut extends DispositionAbstractBase +{ + /* + * @see org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.Status, org.alfresco.web.scripts.Cache) + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + // parse the request to retrieve the schedule object + DispositionSchedule schedule = parseRequestForSchedule(req); + + // parse the request to retrieve the action definition object + DispositionActionDefinition actionDef = parseRequestForActionDefinition(req, schedule); + + // retrieve the rest of the post body and update the action definition + JSONObject json = null; + try + { + json = new JSONObject(new JSONTokener(req.getContent().getContent())); + actionDef = updateActionDefinition(actionDef, json); + } + catch (IOException iox) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Could not read content from req.", iox); + } + catch (JSONException je) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Could not parse JSON from req.", je); + } + + // create model object with just the action data + Map model = new HashMap(1); + model.put("action", createActionDefModel(actionDef, req.getURL())); + return model; + } + + /** + * Updates a dispositionActionDefinition node in the repo. + * + * @param actionDef The action definition to update + * @param json The JSON to use to create the action definition + * @param schedule The DispositionSchedule the action definition belongs to + * @return The updated DispositionActionDefinition + */ + protected DispositionActionDefinition updateActionDefinition(DispositionActionDefinition actionDef, + JSONObject json) throws JSONException + { + // create the properties for the action definition + Map props = new HashMap(8); + + if (json.has("name")) + { + props.put(RecordsManagementModel.PROP_DISPOSITION_ACTION_NAME, json.getString("name")); + } + + if (json.has("description")) + { + props.put(RecordsManagementModel.PROP_DISPOSITION_DESCRIPTION, json.getString("description")); + } + + if (json.has("period")) + { + props.put(RecordsManagementModel.PROP_DISPOSITION_PERIOD, json.getString("period")); + } + + if (json.has("periodProperty")) + { + QName periodProperty = QName.createQName(json.getString("periodProperty"), this.namespaceService); + props.put(RecordsManagementModel.PROP_DISPOSITION_PERIOD_PROPERTY, periodProperty); + } + + if (json.has("eligibleOnFirstCompleteEvent")) + { + props.put(RecordsManagementModel.PROP_DISPOSITION_EVENT_COMBINATION, + json.getBoolean("eligibleOnFirstCompleteEvent") ? "or" : "and"); + } + + if (json.has("location")) + { + props.put(RecordsManagementModel.PROP_DISPOSITION_LOCATION, + json.getString("location")); + } + + if (json.has("events")) + { + JSONArray events = json.getJSONArray("events"); + List eventsList = new ArrayList(events.length()); + for (int x = 0; x < events.length(); x++) + { + eventsList.add(events.getString(x)); + } + props.put(RecordsManagementModel.PROP_DISPOSITION_EVENT, (Serializable)eventsList); + } + + // update the action definition + return this.dispositionService.updateDispositionActionDefinition(actionDef, props); + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/DispositionLifecycleGet.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/DispositionLifecycleGet.java new file mode 100644 index 0000000000..f6dfe3b76e --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/DispositionLifecycleGet.java @@ -0,0 +1,183 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionAction; +import org.alfresco.module.org_alfresco_module_rm.event.EventCompletionDetails; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.PersonService; +import org.springframework.extensions.surf.util.ISO8601DateFormat; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Implementation for Java backed webscript to return full details + * about a disposition lifecycle (next disposition action). + * + * @author Gavin Cornwell + */ +public class DispositionLifecycleGet extends DispositionAbstractBase +{ + PersonService personService; + + /** + * Sets the PersonService instance + * + * @param personService The PersonService instance + */ + public void setPersonService(PersonService personService) + { + this.personService = personService; + } + + /* + * @see org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.Status, org.alfresco.web.scripts.Cache) + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + // parse the request to retrieve the next action + NodeRef nodeRef = parseRequestForNodeRef(req); + + // make sure the node passed in has a next action attached + DispositionAction nextAction = this.dispositionService.getNextDispositionAction(nodeRef); + if (nextAction == null) + { + status.setCode(HttpServletResponse.SC_NOT_FOUND, + "Node " + nodeRef.toString() + " does not have a disposition lifecycle"); + return null; + } + else + { + // add all the next action data to Map + Map nextActionModel = new HashMap(8); + String serviceUrl = req.getServiceContextPath() + req.getPathInfo(); + nextActionModel.put("url", serviceUrl); + nextActionModel.put("name", nextAction.getName()); + nextActionModel.put("label", nextAction.getLabel()); + nextActionModel.put("eventsEligible", this.dispositionService.isNextDispositionActionEligible(nodeRef)); + + if (nextAction.getAsOfDate() != null) + { + nextActionModel.put("asOf", ISO8601DateFormat.format(nextAction.getAsOfDate())); + } + + if (nextAction.getStartedAt() != null) + { + nextActionModel.put("startedAt", ISO8601DateFormat.format(nextAction.getStartedAt())); + } + + String startedBy = nextAction.getStartedBy(); + if (startedBy != null) + { + nextActionModel.put("startedBy", startedBy); + addUsersRealName(nextActionModel, startedBy, "startedBy"); + } + + if (nextAction.getCompletedAt() != null) + { + nextActionModel.put("completedAt", ISO8601DateFormat.format(nextAction.getCompletedAt())); + } + + String completedBy = nextAction.getCompletedBy(); + if (completedBy != null) + { + nextActionModel.put("completedBy", completedBy); + addUsersRealName(nextActionModel, completedBy, "completedBy"); + } + + List> events = new ArrayList>(); + for (EventCompletionDetails event : nextAction.getEventCompletionDetails()) + { + events.add(createEventModel(event)); + } + nextActionModel.put("events", events); + + // create model object with just the schedule data + Map model = new HashMap(1); + model.put("nextaction", nextActionModel); + return model; + } + } + + /** + * Helper to create a model to represent the given event execution. + * + * @param event The event to create a model for + * @return Map representing the model + */ + protected Map createEventModel(EventCompletionDetails event) + { + Map model = new HashMap(8); + + model.put("name", event.getEventName()); + model.put("label", event.getEventLabel()); + model.put("automatic", event.isEventExecutionAutomatic()); + model.put("complete", event.isEventComplete()); + + String completedBy = event.getEventCompletedBy(); + if (completedBy != null) + { + model.put("completedBy", completedBy); + addUsersRealName(model, completedBy, "completedBy"); + } + + if (event.getEventCompletedAt() != null) + { + model.put("completedAt", ISO8601DateFormat.format(event.getEventCompletedAt())); + } + + return model; + } + + /** + * Adds the given username's first and last name to the given model. + * + * @param model The model to add the first and last name to + * @param userName The username of the user to lookup + * @param propertyPrefix The prefix of the property name to use when adding to the model + */ + protected void addUsersRealName(Map model, String userName, String propertyPrefix) + { + NodeRef user = this.personService.getPerson(userName); + if (user != null) + { + String firstName = (String)this.nodeService.getProperty(user, ContentModel.PROP_FIRSTNAME); + if (firstName != null) + { + model.put(propertyPrefix + "FirstName", firstName); + } + + String lastName = (String)this.nodeService.getProperty(user, ContentModel.PROP_LASTNAME); + if (lastName != null) + { + model.put(propertyPrefix + "LastName", lastName); + } + } + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/DispositionScheduleGet.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/DispositionScheduleGet.java new file mode 100644 index 0000000000..c9775259ef --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/DispositionScheduleGet.java @@ -0,0 +1,98 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionActionDefinition; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.service.cmr.repository.NodeRef; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Implementation for Java backed webscript to return full details + * about a disposition schedule. + * + * @author Gavin Cornwell + */ +public class DispositionScheduleGet extends DispositionAbstractBase +{ + /* + * @see org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.Status, org.alfresco.web.scripts.Cache) + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + // parse the request to retrieve the schedule object + DispositionSchedule schedule = parseRequestForSchedule(req); + + // add all the schedule data to Map + Map scheduleModel = new HashMap(8); + + // build url + String serviceUrl = req.getServiceContextPath() + req.getPathInfo(); + scheduleModel.put("url", serviceUrl); + String actionsUrl = serviceUrl + "/dispositionactiondefinitions"; + scheduleModel.put("actionsUrl", actionsUrl); + scheduleModel.put("nodeRef", schedule.getNodeRef().toString()); + scheduleModel.put("recordLevelDisposition", schedule.isRecordLevelDisposition()); + scheduleModel.put("canStepsBeRemoved", + !this.dispositionService.hasDisposableItems(schedule)); + + if (schedule.getDispositionAuthority() != null) + { + scheduleModel.put("authority", schedule.getDispositionAuthority()); + } + + if (schedule.getDispositionInstructions() != null) + { + scheduleModel.put("instructions", schedule.getDispositionInstructions()); + } + + boolean unpublishedUpdates = false; + boolean publishInProgress = false; + + List> actions = new ArrayList>(); + for (DispositionActionDefinition actionDef : schedule.getDispositionActionDefinitions()) + { + NodeRef actionDefNodeRef = actionDef.getNodeRef(); + if (nodeService.hasAspect(actionDefNodeRef, RecordsManagementModel.ASPECT_UNPUBLISHED_UPDATE) == true) + { + unpublishedUpdates = true; + publishInProgress = ((Boolean)nodeService.getProperty(actionDefNodeRef, RecordsManagementModel.PROP_PUBLISH_IN_PROGRESS)).booleanValue(); + } + + actions.add(createActionDefModel(actionDef, actionsUrl + "/" + actionDef.getId())); + } + scheduleModel.put("actions", actions); + scheduleModel.put("unpublishedUpdates", unpublishedUpdates); + scheduleModel.put("publishInProgress", publishInProgress); + + // create model object with just the schedule data + Map model = new HashMap(1); + model.put("schedule", scheduleModel); + return model; + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/DodCustomTypesGet.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/DodCustomTypesGet.java new file mode 100644 index 0000000000..f35ebd0319 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/DodCustomTypesGet.java @@ -0,0 +1,69 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.dod5015.DOD5015Model; +import org.alfresco.service.cmr.dictionary.AspectDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class provides the implementation for the dodcustomtypes.get webscript. + * + * @author Neil McErlean + */ +public class DodCustomTypesGet extends DeclarativeWebScript +{ + // TODO Investigate a way of not hard-coding the 4 custom types here. + private final static List customTypeAspects = Arrays.asList(new QName[]{DOD5015Model.ASPECT_SCANNED_RECORD, + DOD5015Model.ASPECT_PDF_RECORD, DOD5015Model.ASPECT_DIGITAL_PHOTOGRAPH_RECORD, DOD5015Model.ASPECT_WEB_RECORD}); + + private DictionaryService dictionaryService; + + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + @Override + public Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(); + + List customTypeAspectDefinitions = new ArrayList(4); + for (QName aspectQName : customTypeAspects) + { + AspectDefinition nextAspectDef = dictionaryService.getAspect(aspectQName); + customTypeAspectDefinitions.add(nextAspectDef); + } + model.put("dodCustomTypes", customTypeAspectDefinitions); + + return model; + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/EmailMapGet.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/EmailMapGet.java new file mode 100644 index 0000000000..7f225e5cac --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/EmailMapGet.java @@ -0,0 +1,67 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.alfresco.module.org_alfresco_module_rm.email.CustomEmailMappingService; +import org.alfresco.module.org_alfresco_module_rm.email.CustomMapping; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Implementation for Java backed webscript to return + * custom email field mappings + */ +public class EmailMapGet extends DeclarativeWebScript +{ + + /* + * @see org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.Status, org.alfresco.web.scripts.Cache) + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + // String requestUrl = req.getURL(); + + Set emailMap = customEmailMappingService.getCustomMappings(); + + // create model object with the lists model + Map model = new HashMap(1); + model.put("emailmap", emailMap); + return model; + } + + private CustomEmailMappingService customEmailMappingService; + + public void setCustomEmailMappingService(CustomEmailMappingService customEmailMappingService) + { + this.customEmailMappingService = customEmailMappingService; + } + + public CustomEmailMappingService getCustomEmailMappingService() + { + return customEmailMappingService; + } + +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/EmailMapPost.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/EmailMapPost.java new file mode 100644 index 0000000000..a712cc2e64 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/EmailMapPost.java @@ -0,0 +1,110 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.alfresco.module.org_alfresco_module_rm.email.CustomEmailMappingService; +import org.alfresco.module.org_alfresco_module_rm.email.CustomMapping; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Implementation for Java backed webscript to return + * custom email field mappings + */ +public class EmailMapPost extends DeclarativeWebScript +{ + /* + * @see org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.Status, org.alfresco.web.scripts.Cache) + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + + try + { + JSONObject json = null; + json = new JSONObject(new JSONTokener(req.getContent().getContent())); + + if(json.has("add")) + { + JSONArray toAdd = json.getJSONArray("add"); + for(int i = 0 ; i < toAdd.length(); i++) + { + JSONObject val = toAdd.getJSONObject(i); + customEmailMappingService.addCustomMapping(val.getString("from"), val.getString("to")); + + } + } + + if(json.has("delete")) + { + JSONArray toDelete = json.getJSONArray("delete"); + for(int i = 0 ; i < toDelete.length(); i++) + { + JSONObject val = toDelete.getJSONObject(i); + customEmailMappingService.deleteCustomMapping(val.getString("from"), val.getString("to")); + } + } + + + // Set the return value. + Set emailMap = customEmailMappingService.getCustomMappings(); + // create model object with the lists model + Map model = new HashMap(1); + model.put("emailmap", emailMap); + return model; + } + catch (IOException iox) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Could not read content from req.", iox); + } + catch (JSONException je) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Could not parse JSON from req.", je); + } + } + + private CustomEmailMappingService customEmailMappingService; + + public void setCustomEmailMappingService(CustomEmailMappingService customEmailMappingService) + { + this.customEmailMappingService = customEmailMappingService; + } + + public CustomEmailMappingService getCustomEmailMappingService() + { + return customEmailMappingService; + } + + +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/EmailMapPut.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/EmailMapPut.java new file mode 100644 index 0000000000..f49a8bfa9b --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/EmailMapPut.java @@ -0,0 +1,108 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.alfresco.module.org_alfresco_module_rm.email.CustomEmailMappingService; +import org.alfresco.module.org_alfresco_module_rm.email.CustomMapping; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Implementation for Java backed webscript to return + * custom email field mappings + */ +public class EmailMapPut extends DeclarativeWebScript +{ + /* + * @see org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.Status, org.alfresco.web.scripts.Cache) + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + + try + { + JSONObject json = null; + json = new JSONObject(new JSONTokener(req.getContent().getContent())); + + if(json.has("add")) + { + JSONArray toAdd = json.getJSONArray("add"); + for(int i = 0 ; i < toAdd.length(); i++) + { + JSONObject val = toAdd.getJSONObject(i); + customEmailMappingService.addCustomMapping(val.getString("from"), val.getString("to")); + + } + } + + if(json.has("delete")) + { + JSONArray toDelete = json.getJSONArray("delete"); + for(int i = 0 ; i < toDelete.length(); i++) + { + JSONObject val = toDelete.getJSONObject(i); + customEmailMappingService.deleteCustomMapping(val.getString("from"), val.getString("to")); + } + } + + + // Set the return value. + Set emailMap = customEmailMappingService.getCustomMappings(); + // create model object with the lists model + Map model = new HashMap(1); + model.put("emailmap", emailMap); + return model; + } + catch (IOException iox) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Could not read content from req.", iox); + } + catch (JSONException je) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Could not parse JSON from req.", je); + } + } + + private CustomEmailMappingService customEmailMappingService; + + public void setCustomEmailMappingService(CustomEmailMappingService customEmailMappingService) + { + this.customEmailMappingService = customEmailMappingService; + } + + public CustomEmailMappingService getCustomEmailMappingService() + { + return customEmailMappingService; + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/ExportPost.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/ExportPost.java new file mode 100644 index 0000000000..b4b4c09303 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/ExportPost.java @@ -0,0 +1,155 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +import java.io.File; +import java.io.IOException; +import java.io.StringWriter; +import java.io.PrintWriter; + +import org.alfresco.model.ContentModel; +import org.alfresco.model.RenditionModel; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementSearchBehaviour; +import org.alfresco.repo.exporter.ACPExportPackageHandler; +import org.alfresco.repo.web.scripts.content.StreamACP; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.view.ExporterCrawlerParameters; +import org.alfresco.service.cmr.view.Location; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; + +/** + * Creates an RM specific ACP file of nodes to export then streams it back + * to the client. + * + * @author Gavin Cornwell + */ +public class ExportPost extends StreamACP +{ + /** Logger */ + private static Log logger = LogFactory.getLog(ExportPost.class); + + protected static final String PARAM_TRANSFER_FORMAT = "transferFormat"; + + /** + * @see org.alfresco.web.scripts.WebScript#execute(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.WebScriptResponse) + */ + @SuppressWarnings("deprecation") + @Override + public void execute(WebScriptRequest req, WebScriptResponse res) throws IOException + { + File tempACPFile = null; + try + { + NodeRef[] nodeRefs = null; + boolean transferFormat = false; + String contentType = req.getContentType(); + if (MULTIPART_FORMDATA.equals(contentType)) + { + // get nodeRefs parameter from form + nodeRefs = getNodeRefs(req.getParameter(PARAM_NODE_REFS)); + + // look for the transfer format + String transferFormatParam = req.getParameter(PARAM_TRANSFER_FORMAT); + if (transferFormatParam != null && transferFormatParam.length() > 0) + { + transferFormat = Boolean.parseBoolean(transferFormatParam); + } + } + else + { + // presume the request is a JSON request so get nodeRefs from JSON body + JSONObject json = new JSONObject(new JSONTokener(req.getContent().getContent())); + nodeRefs = getNodeRefs(json); + + if (json.has(PARAM_TRANSFER_FORMAT)) + { + transferFormat = json.getBoolean(PARAM_TRANSFER_FORMAT); + } + } + + // setup the ACP parameters + ExporterCrawlerParameters params = new ExporterCrawlerParameters(); + params.setCrawlSelf(true); + params.setCrawlChildNodes(true); + params.setExportFrom(new Location(nodeRefs)); + + // if transfer format has been requested we need to exclude certain aspects + if (transferFormat) + { + // restrict specific aspects from being returned + QName[] excludedAspects = new QName[] { + RenditionModel.ASPECT_RENDITIONED, + ContentModel.ASPECT_THUMBNAILED, + RecordsManagementModel.ASPECT_DISPOSITION_LIFECYCLE, + RecordsManagementSearchBehaviour.ASPECT_RM_SEARCH}; + params.setExcludeAspects(excludedAspects); + } + + // create an ACP of the nodes + tempACPFile = createACP(params, + transferFormat ? ZIP_EXTENSION : ACPExportPackageHandler.ACP_EXTENSION, + transferFormat); + + // stream the ACP back to the client as an attachment (forcing save as) + streamContent(req, res, tempACPFile, true, tempACPFile.getName()); + } + catch (IOException ioe) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Could not read content from req.", ioe); + } + catch (JSONException je) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Could not parse JSON from req.", je); + } + catch(Throwable e) + { + if (logger.isDebugEnabled()) + { + StringWriter stack = new StringWriter(); + e.printStackTrace(new PrintWriter(stack)); + logger.debug("Caught exception; decorating with appropriate status template : " + stack.toString()); + } + + throw createStatusException(e, req, res); + } + finally + { + // try and delete the temporary file + if (tempACPFile != null) + { + if (logger.isDebugEnabled()) + logger.debug("Deleting temporary archive: " + tempACPFile.getAbsolutePath()); + + tempACPFile.delete(); + } + } + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/ImportPost.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/ImportPost.java new file mode 100644 index 0000000000..dfe1a2c8de --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/ImportPost.java @@ -0,0 +1,252 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService; +import org.alfresco.repo.exporter.ACPExportPackageHandler; +import org.alfresco.repo.importer.ACPImportPackageHandler; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.view.ImporterService; +import org.alfresco.service.cmr.view.Location; +import org.alfresco.util.TempFileProvider; +import org.springframework.extensions.webscripts.Cache; +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.WrappingWebScriptRequest; +import org.springframework.extensions.webscripts.servlet.WebScriptServletRequest; +import org.springframework.extensions.webscripts.servlet.FormData.FormField; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.util.FileCopyUtils; + +/** + * Imports an ACP file into a records management container. + * + * @author Gavin Cornwell + */ +public class ImportPost extends DeclarativeWebScript +{ + /** Logger */ + private static Log logger = LogFactory.getLog(ImportPost.class); + + protected static final String MULTIPART_FORMDATA = "multipart/form-data"; + protected static final String PARAM_DESTINATION = "destination"; + protected static final String PARAM_ARCHIVE = "archive"; + protected static final String TEMP_FILE_PREFIX = "import_"; + + protected NodeService nodeService; + protected DictionaryService dictionaryService; + protected ImporterService importerService; + protected RecordsManagementService rmService; + protected RecordsManagementSecurityService rmSecurityService; + + /** + * @param nodeService + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Sets the data dictionary service + * + * @param dictionaryService The DictionaryService instance + */ + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + /** + * Sets the ImporterService to use + * + * @param importerService The ImporterService + */ + public void setImporterService(ImporterService importerService) + { + this.importerService = importerService; + } + + /** + * Sets the RecordsManagementSecurityService instance + * + * @param rmSecurityService The RecordsManagementSecurityService instance + */ + public void setRecordsManagementSecurityService(RecordsManagementSecurityService rmSecurityService) + { + this.rmSecurityService = rmSecurityService; + } + + /** + * Sets the RecordsManagementService instance + * + * @param rmService The RecordsManagementService instance + */ + public void setRecordsManagementService(RecordsManagementService rmService) + { + this.rmService = rmService; + } + + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + // Unwrap to a WebScriptServletRequest if we have one + WebScriptServletRequest webScriptServletRequest = null; + WebScriptRequest current = req; + do + { + if (current instanceof WebScriptServletRequest) + { + webScriptServletRequest = (WebScriptServletRequest) current; + current = null; + } + else if (current instanceof WrappingWebScriptRequest) + { + current = ((WrappingWebScriptRequest) req).getNext(); + } + else + { + current = null; + } + } + while (current != null); + + // get the content type of request and ensure it's multipart/form-data + String contentType = req.getContentType(); + if (MULTIPART_FORMDATA.equals(contentType) && webScriptServletRequest != null) + { + String nodeRef = req.getParameter(PARAM_DESTINATION); + + if (nodeRef == null || nodeRef.length() == 0) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Mandatory 'destination' parameter was not provided in form data"); + } + + // create and check noderef + final NodeRef destination = new NodeRef(nodeRef); + if (nodeService.exists(destination)) + { + // check the destination is an RM container + if (!nodeService.hasAspect(destination, RecordsManagementModel.ASPECT_FILE_PLAN_COMPONENT) || + !dictionaryService.isSubClass(nodeService.getType(destination), ContentModel.TYPE_FOLDER)) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "NodeRef '" + destination + "' does not represent an Records Management container node."); + } + } + else + { + status.setCode(HttpServletResponse.SC_NOT_FOUND, + "NodeRef '" + destination + "' does not exist."); + } + + // as there is no 'import capability' and the RM admin user is different from + // the DM admin user (meaning the webscript 'admin' authentication can't be used) + // perform a manual check here to ensure the current user has the RM admin role. + boolean isAdmin = this.rmSecurityService.hasRMAdminRole( + this.rmService.getFilePlan(destination), + AuthenticationUtil.getRunAsUser()); + if (!isAdmin) + { + throw new WebScriptException(Status.STATUS_FORBIDDEN, "Access Denied"); + } + + File acpFile = null; + try + { + // create a temporary file representing uploaded ACP file + FormField acpContent = webScriptServletRequest.getFileField(PARAM_ARCHIVE); + if (acpContent == null) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Mandatory 'archive' file content was not provided in form data"); + } + + acpFile = TempFileProvider.createTempFile(TEMP_FILE_PREFIX, "." + ACPExportPackageHandler.ACP_EXTENSION); + + // copy contents of uploaded file to temp ACP file + FileOutputStream fos = new FileOutputStream(acpFile); + FileCopyUtils.copy(acpContent.getInputStream(), fos); // NOTE: this method closes both streams + + if (logger.isDebugEnabled()) + logger.debug("Importing uploaded ACP (" + acpFile.getAbsolutePath() + ") into " + nodeRef); + + // setup the import handler + final ACPImportPackageHandler importHandler = new ACPImportPackageHandler(acpFile, "UTF-8"); + + // import the ACP file as the system user + AuthenticationUtil.runAs(new RunAsWork() + { + public NodeRef doWork() throws Exception + { + importerService.importView(importHandler, new Location(destination), null, null); + return null; + } + }, AuthenticationUtil.getSystemUserName()); + + // create and return model + Map model = new HashMap(1); + model.put("success", true); + return model; + } + catch (FileNotFoundException fnfe) + { + throw new WebScriptException(Status.STATUS_INTERNAL_SERVER_ERROR, + "Failed to import ACP file", fnfe); + } + catch (IOException ioe) + { + throw new WebScriptException(Status.STATUS_INTERNAL_SERVER_ERROR, + "Failed to import ACP file", ioe); + } + finally + { + if (acpFile != null) + { + acpFile.delete(); + } + } + } + else + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Request is not " + MULTIPART_FORMDATA + " encoded"); + } + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/ListOfValuesGet.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/ListOfValuesGet.java new file mode 100644 index 0000000000..c313aded0c --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/ListOfValuesGet.java @@ -0,0 +1,304 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction; +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService; +import org.alfresco.module.org_alfresco_module_rm.audit.AuditEvent; +import org.alfresco.module.org_alfresco_module_rm.audit.RecordsManagementAuditService; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService; +import org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEvent; +import org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEventService; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.repository.Period; +import org.alfresco.service.cmr.repository.PeriodProvider; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.util.StringUtils; + +/** + * Implementation for Java backed webscript to return lists + * of values for various records management services. + * + * @author Gavin Cornwell + */ +public class ListOfValuesGet extends DeclarativeWebScript +{ + protected RecordsManagementService rmService; + protected RecordsManagementActionService rmActionService; + protected RecordsManagementAuditService rmAuditService; + protected RecordsManagementEventService rmEventService; + protected DispositionService dispositionService; + protected DictionaryService ddService; + protected NamespaceService namespaceService; + + /** + * Sets the RecordsManagementService instance + * + * @param rmService The RecordsManagementService instance + */ + public void setRecordsManagementService(RecordsManagementService rmService) + { + this.rmService = rmService; + } + + /** + * Sets the RecordsManagementActionService instance + * + * @param rmActionService The RecordsManagementActionService instance + */ + public void setRecordsManagementActionService(RecordsManagementActionService rmActionService) + { + this.rmActionService = rmActionService; + } + + /** + * Sets the RecordsManagementAuditService instance + * + * @param rmAuditService The RecordsManagementAuditService instance + */ + public void setRecordsManagementAuditService(RecordsManagementAuditService rmAuditService) + { + this.rmAuditService = rmAuditService; + } + + /** + * Sets the RecordsManagementEventService instance + * + * @param rmEventService The RecordsManagementEventService instance + */ + public void setRecordsManagementEventService(RecordsManagementEventService rmEventService) + { + this.rmEventService = rmEventService; + } + + /** + * Sets the disposition service + * + * @param dispositionService the disposition service + */ + public void setDispositionService(DispositionService dispositionService) + { + this.dispositionService = dispositionService; + } + + /** + * Sets the DictionaryService instance + * + * @param ddService The DictionaryService instance + */ + public void setDictionaryService(DictionaryService ddService) + { + this.ddService = ddService; + } + + /** + * Sets the NamespaceService instance + * + * @param namespaceService The NamespaceService instance + */ + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + /* + * @see org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.Status, org.alfresco.web.scripts.Cache) + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + // add all the lists data to a Map + Map listsModel = new HashMap(4); + String requestUrl = req.getURL(); + listsModel.put("dispositionActions", createDispositionActionsModel(requestUrl)); + listsModel.put("events", createEventsModel(requestUrl)); + listsModel.put("periodTypes", createPeriodTypesModel(requestUrl)); + listsModel.put("periodProperties", createPeriodPropertiesModel(requestUrl)); + listsModel.put("auditEvents", createAuditEventsModel(requestUrl)); + + // create model object with the lists model + Map model = new HashMap(1); + model.put("lists", listsModel); + return model; + } + + /** + * Creates the model for the list of disposition actions. + * + * @param baseUrl The base URL of the service + * @return model of disposition actions list + */ + protected Map createDispositionActionsModel(String baseUrl) + { + // iterate over the disposition actions + List dispositionActions = this.rmActionService.getDispositionActions(); + List> items = new ArrayList>(dispositionActions.size()); + for (RecordsManagementAction dispositionAction : dispositionActions) + { + Map item = new HashMap(2); + item.put("label", dispositionAction.getLabel()); + item.put("value", dispositionAction.getName()); + items.add(item); + } + + // create the model + Map model = new HashMap(2); + model.put("url", baseUrl + "/dispositionactions"); + model.put("items", items); + + return model; + } + + /** + * Creates the model for the list of events. + * + * @param baseUrl The base URL of the service + * @return model of events list + */ + protected Map createEventsModel(String baseUrl) + { + // get all the events including their display labels from the event service + List events = this.rmEventService.getEvents(); + List> items = new ArrayList>(events.size()); + for (RecordsManagementEvent event : events) + { + Map item = new HashMap(3); + item.put("label", event.getDisplayLabel()); + item.put("value", event.getName()); + item.put("automatic", + this.rmEventService.getEventType(event.getType()).isAutomaticEvent()); + items.add(item); + } + + // create the model + Map model = new HashMap(2); + model.put("url", baseUrl + "/events"); + model.put("items", items); + + return model; + } + + /** + * Creates the model for the list of period types. + * + * @param baseUrl The base URL of the service + * @return model of period types list + */ + protected Map createPeriodTypesModel(String baseUrl) + { + // iterate over all period provides, but ignore 'cron' + Set providers = Period.getProviderNames(); + List> items = new ArrayList>(providers.size()); + for (String provider : providers) + { + PeriodProvider pp = Period.getProvider(provider); + if (!pp.getPeriodType().equals("cron")) + { + Map item = new HashMap(2); + item.put("label", pp.getDisplayLabel()); + item.put("value", pp.getPeriodType()); + items.add(item); + } + } + + // create the model + Map model = new HashMap(2); + model.put("url", baseUrl + "/periodtypes"); + model.put("items", items); + + return model; + } + + /** + * Creates the model for the list of period properties. + * + * @param baseUrl The base URL of the service + * @return model of period properties list + */ + protected Map createPeriodPropertiesModel(String baseUrl) + { + // iterate over all period properties and get the label from their type definition + List periodProperties = dispositionService.getDispositionPeriodProperties(); + List> items = new ArrayList>(periodProperties.size()); + for (QName periodProperty : periodProperties) + { + PropertyDefinition propDef = this.ddService.getProperty(periodProperty); + + if (propDef != null) + { + Map item = new HashMap(2); + String propTitle = propDef.getTitle(); + if (propTitle == null || propTitle.length() == 0) + { + propTitle = StringUtils.capitalize(periodProperty.getLocalName()); + } + item.put("label", propTitle); + item.put("value", periodProperty.toPrefixString(this.namespaceService)); + items.add(item); + } + } + + // create the model + Map model = new HashMap(2); + model.put("url", baseUrl + "/periodproperties"); + model.put("items", items); + + return model; + } + + /** + * Creates the model for the list of audit events. + * + * @param baseUrl The base URL of the service + * @return model of audit events list + */ + protected Map createAuditEventsModel(String baseUrl) + { + // iterate over all audit events + List auditEvents = this.rmAuditService.getAuditEvents(); + List> items = new ArrayList>(auditEvents.size()); + for (AuditEvent event : auditEvents) + { + Map item = new HashMap(2); + item.put("label", event.getLabel()); + item.put("value", event.getName()); + items.add(item); + } + + // create the model + Map model = new HashMap(2); + model.put("url", baseUrl + "/auditevents"); + model.put("items", items); + + return model; + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/RMConstraintGet.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/RMConstraintGet.java new file mode 100644 index 0000000000..dc8cb88530 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/RMConstraintGet.java @@ -0,0 +1,71 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.caveat.RMCaveatConfigService; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Implementation for Java backed webscript to return + * the values for an RM constraint. + */ +public class RMConstraintGet extends DeclarativeWebScript +{ + + /* + * @see org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.Status, org.alfresco.web.scripts.Cache) + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + String requestUrl = req.getURL(); + String extensionPath = req.getExtensionPath(); + + String constraintName = extensionPath.replace('_', ':'); + + List values = caveatConfigService.getRMAllowedValues(constraintName); + + // create model object with the lists model + Map model = new HashMap(1); + model.put("allowedValuesForCurrentUser", values); + model.put("constraintName", extensionPath); + + return model; + } + + public void setCaveatConfigService(RMCaveatConfigService caveatConfigService) + { + this.caveatConfigService = caveatConfigService; + } + + public RMCaveatConfigService getCaveatConfigService() + { + return caveatConfigService; + } + + private RMCaveatConfigService caveatConfigService; + +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/RecordMetaDataAspectsGet.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/RecordMetaDataAspectsGet.java new file mode 100644 index 0000000000..a15ac40173 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/RecordMetaDataAspectsGet.java @@ -0,0 +1,107 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.service.cmr.dictionary.AspectDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +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 Roy Wetherall + */ +public class RecordMetaDataAspectsGet extends DeclarativeWebScript +{ + protected DictionaryService dictionaryService; + protected NamespaceService namespaceService; + protected RecordsManagementService recordsManagementService; + + /** + * Set the dictionary service instance + * + * @param dictionaryService the {@link DictionaryService} instance + */ + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + /** + * Sets the {@link NamespaceService} instance + * + * @param namespaceService The {@link NamespaceService} instance + */ + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + public void setRecordsManagementService(RecordsManagementService recordsManagementService) + { + this.recordsManagementService = recordsManagementService; + } + + /* + * @see org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.Status, org.alfresco.web.scripts.Cache) + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + // Get the details of all the aspects + Set aspectQNames = recordsManagementService.getRecordMetaDataAspects(); + List> aspects = new ArrayList>(aspectQNames.size()+1); + for (QName aspectQName : aspectQNames) + { + // Get the prefix aspect and default the label to the localname + String prefixString = aspectQName.toPrefixString(namespaceService); + String label = aspectQName.getLocalName(); + + Map aspect = new HashMap(2); + aspect.put("id", prefixString); + + // Try and get the aspect definition + AspectDefinition aspectDefinition = dictionaryService.getAspect(aspectQName); + if (aspectDefinition != null) + { + // Fet the label from the aspect definition + label = aspectDefinition.getTitle(); + } + aspect.put("value", label); + + // Add the aspect details to the aspects list + aspects.add(aspect); + } + + // create model object with the lists model + Map model = new HashMap(1); + model.put("aspects", aspects); + return model; + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/RmActionPost.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/RmActionPost.java new file mode 100644 index 0000000000..616410a690 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/RmActionPost.java @@ -0,0 +1,224 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +import java.io.IOException; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionResult; +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.springframework.extensions.surf.util.ISO8601DateFormat; +import org.springframework.extensions.webscripts.Cache; +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.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; + +/** + * This class provides the implementation for the rmaction webscript. + * + * @author Neil McErlean + */ +public class RmActionPost extends DeclarativeWebScript +{ + private static Log logger = LogFactory.getLog(RmActionPost.class); + + private static final String PARAM_NAME = "name"; + private static final String PARAM_NODE_REF = "nodeRef"; + private static final String PARAM_NODE_REFS = "nodeRefs"; + private static final String PARAM_PARAMS = "params"; + + private NodeService nodeService; + private RecordsManagementActionService rmActionService; + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setRecordsManagementActionService(RecordsManagementActionService rmActionService) + { + this.rmActionService = rmActionService; + } + + @SuppressWarnings("unchecked") + @Override + public Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + String reqContentAsString; + try + { + reqContentAsString = req.getContent().getContent(); + } + catch (IOException iox) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Could not read content from req.", iox); + } + + String actionName = null; + List targetNodeRefs = null; + Map actionParams = new HashMap(3); + + try + { + JSONObject jsonObj = new JSONObject(new JSONTokener(reqContentAsString)); + + // Get the action name + if (jsonObj.has(PARAM_NAME) == true) + { + actionName = jsonObj.getString(PARAM_NAME); + } + + // Get the target references + if (jsonObj.has(PARAM_NODE_REF) == true) + { + NodeRef nodeRef = new NodeRef(jsonObj.getString(PARAM_NODE_REF)); + targetNodeRefs = new ArrayList(1); + targetNodeRefs.add(nodeRef); + } + if (jsonObj.has(PARAM_NODE_REFS) == true) + { + JSONArray jsonArray = jsonObj.getJSONArray(PARAM_NODE_REFS); + if (jsonArray.length() != 0) + { + targetNodeRefs = new ArrayList(jsonArray.length()); + for (int i = 0; i < jsonArray.length(); i++) + { + NodeRef nodeRef = new NodeRef(jsonArray.getString(i)); + targetNodeRefs.add(nodeRef); + } + } + } + + // params are optional. + if (jsonObj.has(PARAM_PARAMS)) + { + JSONObject paramsObj = jsonObj.getJSONObject(PARAM_PARAMS); + for (Iterator iter = paramsObj.keys(); iter.hasNext(); ) + { + Object nextKey = iter.next(); + String nextKeyString = (String)nextKey; + Object nextValue = paramsObj.get(nextKeyString); + + // Check for date values + if (nextValue instanceof JSONObject) + { + if (((JSONObject)nextValue).has("iso8601") == true) + { + String dateStringValue = ((JSONObject)nextValue).getString("iso8601"); + nextValue = ISO8601DateFormat.parse(dateStringValue); + } + } + + actionParams.put(nextKeyString, (Serializable)nextValue); + } + } + } + catch (JSONException exception) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Unable to parse request JSON."); + } + + // validate input: check for mandatory params. + // Some RM actions can be posted without a nodeRef. + if (actionName == null) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "A mandatory parameter has not been provided in URL"); + } + + // Check that all the nodes provided exist and build report string + StringBuffer targetNodeRefsString = new StringBuffer(30); + boolean firstTime = true; + for (NodeRef targetNodeRef : targetNodeRefs) + { + if (nodeService.exists(targetNodeRef) == false) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, + "The targetNode does not exist (" + targetNodeRef.toString() + ")"); + } + + // Build the string + if (firstTime == true) + { + firstTime = false; + } + else + { + targetNodeRefsString.append(", "); + } + targetNodeRefsString.append(targetNodeRef.toString()); + } + + // Proceed to execute the specified action on the specified node. + if (logger.isDebugEnabled()) + { + StringBuilder msg = new StringBuilder(); + msg.append("Executing Record Action ") + .append(actionName) + .append(", (") + .append(targetNodeRefsString.toString()) + .append("), ") + .append(actionParams); + logger.debug(msg.toString()); + } + + Map model = new HashMap(); + if (targetNodeRefs.isEmpty()) + { + RecordsManagementActionResult result = this.rmActionService.executeRecordsManagementAction(actionName, actionParams); + if (result.getValue() != null) + { + model.put("result", result.getValue().toString()); + } + } + else + { + Map resultMap = this.rmActionService.executeRecordsManagementAction(targetNodeRefs, actionName, actionParams); + Map results = new HashMap(resultMap.size()); + for (NodeRef nodeRef : resultMap.keySet()) + { + Object value = resultMap.get(nodeRef).getValue(); + if (value != null) + { + results.put(nodeRef.toString(), resultMap.get(nodeRef).getValue().toString()); + } + } + model.put("results", results); + } + + model.put("message", "Successfully queued action [" + actionName + "] on " + targetNodeRefsString.toString()); + + return model; + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/TransferGet.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/TransferGet.java new file mode 100644 index 0000000000..022ad7bbbe --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/TransferGet.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.module.org_alfresco_module_rm.script; + +import java.io.File; +import java.io.IOException; + +import org.alfresco.model.ContentModel; +import org.alfresco.model.RenditionModel; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementSearchBehaviour; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.view.ExporterCrawlerParameters; +import org.alfresco.service.cmr.view.Location; +import org.alfresco.service.namespace.QName; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Streams the nodes of a transfer object to the client in the form of an + * ACP file. + * + * @author Gavin Cornwell + */ +public class TransferGet extends BaseTransferWebScript +{ + /** Logger */ + private static Log logger = LogFactory.getLog(TransferGet.class); + + @SuppressWarnings("deprecation") + @Override + protected File executeTransfer(NodeRef transferNode, + WebScriptRequest req, WebScriptResponse res, + Status status, Cache cache) throws IOException + { + // get all 'transferred' nodes + NodeRef[] itemsToTransfer = getTransferNodes(transferNode); + + // setup the ACP parameters + ExporterCrawlerParameters params = new ExporterCrawlerParameters(); + params.setCrawlSelf(true); + params.setCrawlChildNodes(true); + params.setExportFrom(new Location(itemsToTransfer)); + QName[] excludedAspects = new QName[] { + RenditionModel.ASPECT_RENDITIONED, + ContentModel.ASPECT_THUMBNAILED, + RecordsManagementModel.ASPECT_DISPOSITION_LIFECYCLE, + RecordsManagementSearchBehaviour.ASPECT_RM_SEARCH}; + params.setExcludeAspects(excludedAspects); + + // create an archive of all the nodes to transfer + File tempFile = createACP(params, ZIP_EXTENSION, true); + + if (logger.isDebugEnabled()) + { + logger.debug("Creating transfer archive for " + itemsToTransfer.length + + " items into file: " + tempFile.getAbsolutePath()); + } + + // stream the archive back to the client as an attachment (forcing save as) + streamContent(req, res, tempFile, true, tempFile.getName()); + + // return the temp file for deletion + return tempFile; + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/TransferReportGet.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/TransferReportGet.java new file mode 100644 index 0000000000..ed8706224a --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/TransferReportGet.java @@ -0,0 +1,287 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Writer; +import java.util.Date; +import java.util.List; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.alfresco.util.TempFileProvider; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.surf.util.ISO8601DateFormat; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; + +/** + * Returns a JSON representation of a transfer report. + * + * @author Gavin Cornwell + */ +public class TransferReportGet extends BaseTransferWebScript +{ + /** Logger */ + private static Log logger = LogFactory.getLog(TransferReportGet.class); + + protected static final String REPORT_FILE_PREFIX = "report_"; + protected static final String REPORT_FILE_SUFFIX = ".json"; + + protected DictionaryService ddService; + protected RecordsManagementService rmService; + protected DispositionService dispositionService; + + /** + * Sets the DictionaryService instance + * + * @param ddService The DictionaryService instance + */ + public void setDictionaryService(DictionaryService ddService) + { + this.ddService = ddService; + } + + /** + * Sets the disposition service + * + * @param dispositionService the disposition service + */ + public void setDispositionService(DispositionService dispositionService) + { + this.dispositionService = dispositionService; + } + + /** + * Sets the RecordsManagementService instance + * + * @param rmService RecordsManagementService instance + */ + public void setRecordsManagementService(RecordsManagementService rmService) + { + this.rmService = rmService; + } + + @Override + protected File executeTransfer(NodeRef transferNode, + WebScriptRequest req, WebScriptResponse res, + Status status, Cache cache) throws IOException + { + // generate the report (will be in JSON format) + File report = generateJSONTransferReport(transferNode); + + // stream the report back to the client + streamContent(req, res, report, false); + + // return the file for deletion + return report; + } + + /** + * Generates a File containing the JSON representation of a transfer report. + * + * @param transferNode The transfer node + * @return File containing JSON representation of a transfer report + * @throws IOException + */ + File generateJSONTransferReport(NodeRef transferNode) throws IOException + { + File report = TempFileProvider.createTempFile(REPORT_FILE_PREFIX, REPORT_FILE_SUFFIX); + Writer writer = null; + try + { + // get all 'transferred' nodes + NodeRef[] itemsToTransfer = getTransferNodes(transferNode); + + if (logger.isDebugEnabled()) + { + logger.debug("Generating JSON transfer report for " + itemsToTransfer.length + + " items into file: " + report.getAbsolutePath()); + } + + // create the writer + writer = new FileWriter(report); + + // use RMService to get disposition authority + String dispositionAuthority = null; + if (itemsToTransfer.length > 0) + { + // use the first transfer item to get to disposition schedule + DispositionSchedule ds = dispositionService.getDispositionSchedule(itemsToTransfer[0]); + if (ds != null) + { + dispositionAuthority = ds.getDispositionAuthority(); + } + } + + // write the JSON header + writer.write("{\n\t\"data\":\n\t{"); + writer.write("\n\t\t\"transferDate\": \""); + writer.write(ISO8601DateFormat.format( + (Date)this.nodeService.getProperty(transferNode, ContentModel.PROP_CREATED))); + writer.write("\",\n\t\t\"transferPerformedBy\": \""); + writer.write(AuthenticationUtil.getRunAsUser()); + writer.write("\",\n\t\t\"dispositionAuthority\": \""); + writer.write(dispositionAuthority != null ? dispositionAuthority : ""); + writer.write("\",\n\t\t\"items\":\n\t\t["); + + // write out JSON representation of items to transfer + generateTransferItemsJSON(writer, itemsToTransfer); + + // write the JSON footer + writer.write("\n\t\t]\n\t}\n}"); + } + finally + { + if (writer != null) + { + try { writer.close(); } catch (IOException ioe) {} + } + } + + return report; + } + + /** + * Generates the JSON to represent the given NodeRefs + * + * @param writer Writer to write to + * @param itemsToTransfer NodeRefs being transferred + * @throws IOException + */ + protected void generateTransferItemsJSON(Writer writer, NodeRef[] itemsToTransfer) + throws IOException + { + boolean first = true; + for (NodeRef item : itemsToTransfer) + { + if (first) + { + first = false; + } + else + { + writer.write(","); + } + + if (ddService.isSubClass(nodeService.getType(item), ContentModel.TYPE_FOLDER)) + { + generateTransferFolderJSON(writer, item); + } + else + { + generateTransferRecordJSON(writer, item); + } + } + } + + /** + * Generates the JSON to represent the given folder. + * + * @param writer Writer to write to + * @param folderNode Folder being transferred + * @throws IOException + */ + protected void generateTransferFolderJSON(Writer writer, NodeRef folderNode) + throws IOException + { + // TODO: Add identation + + writer.write("\n{\n\"type\":\"folder\",\n"); + writer.write("\"name\":\""); + writer.write((String)nodeService.getProperty(folderNode, ContentModel.PROP_NAME)); + writer.write("\",\n\"nodeRef\":\""); + writer.write(folderNode.toString()); + writer.write("\",\n\"id\":\""); + writer.write((String)nodeService.getProperty(folderNode, RecordsManagementModel.PROP_IDENTIFIER)); + writer.write("\",\n\"children\":\n["); + + boolean first = true; + List assocs = this.nodeService.getChildAssocs(folderNode, + ContentModel.ASSOC_CONTAINS, RegexQNamePattern.MATCH_ALL); + for (ChildAssociationRef child : assocs) + { + if (first) + { + first = false; + } + else + { + writer.write(","); + } + + NodeRef childRef = child.getChildRef(); + if (ddService.isSubClass(nodeService.getType(childRef), ContentModel.TYPE_FOLDER)) + { + generateTransferFolderJSON(writer, childRef); + } + else + { + generateTransferRecordJSON(writer, childRef); + } + } + + writer.write("\n]\n}"); + } + + /** + * Generates the JSON to represent the given record. + * + * @param writer Writer to write to + * @param recordNode Record being transferred + * @throws IOException + */ + protected void generateTransferRecordJSON(Writer writer, NodeRef recordNode) + throws IOException + { + writer.write("\n{\n\"type\":\"record\",\n"); + writer.write("\"name\":\""); + writer.write((String)nodeService.getProperty(recordNode, ContentModel.PROP_NAME)); + writer.write("\",\n\"nodeRef\":\""); + writer.write(recordNode.toString()); + writer.write("\",\n\"id\":\""); + writer.write((String)nodeService.getProperty(recordNode, RecordsManagementModel.PROP_IDENTIFIER)); + writer.write("\""); + + if (this.nodeService.hasAspect(recordNode, RecordsManagementModel.ASPECT_DECLARED_RECORD)) + { + writer.write(",\n\"declaredBy\":\""); + writer.write((String)nodeService.getProperty(recordNode, RecordsManagementModel.PROP_DECLARED_BY)); + writer.write("\",\n\"declaredAt\":\""); + writer.write(ISO8601DateFormat.format( + (Date)this.nodeService.getProperty(recordNode, RecordsManagementModel.PROP_DECLARED_AT))); + writer.write("\""); + } + + writer.write("\n}"); + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/TransferReportPost.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/TransferReportPost.java new file mode 100644 index 0000000000..f95dbfa4e9 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/TransferReportPost.java @@ -0,0 +1,428 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Serializable; +import java.io.Writer; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.alfresco.util.TempFileProvider; +import org.apache.commons.lang.StringEscapeUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; +import org.springframework.extensions.surf.util.ParameterCheck; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; + +/** + * Files a transfer report as a record. + * + * @author Gavin Cornwell + */ +public class TransferReportPost extends BaseTransferWebScript +{ + /** Logger */ + private static Log logger = LogFactory.getLog(TransferReportPost.class); + + protected static final String REPORT_FILE_PREFIX = "report_"; + protected static final String REPORT_FILE_SUFFIX = ".html"; + protected static final String PARAM_DESTINATION = "destination"; + protected static final String RESPONSE_SUCCESS = "success"; + protected static final String RESPONSE_RECORD = "record"; + protected static final String RESPONSE_RECORD_NAME = "recordName"; + protected static final String FILE_ACTION = "file"; + + protected DictionaryService ddService; + protected RecordsManagementActionService rmActionService; + protected RecordsManagementService rmService; + protected DispositionService dispositionService; + + /** + * Sets the DictionaryService instance + * + * @param ddService The DictionaryService instance + */ + public void setDictionaryService(DictionaryService ddService) + { + this.ddService = ddService; + } + + /** + * Sets the RecordsManagementService instance + * + * @param rmService RecordsManagementService instance + */ + public void setRecordsManagementService(RecordsManagementService rmService) + { + this.rmService = rmService; + } + + /** + * Sets the disposition service + * + * @param dispositionService disposition service + */ + public void setDispositionService(DispositionService dispositionService) + { + this.dispositionService = dispositionService; + } + + /** + * Sets the RecordsManagementActionService instance + * + * @param rmActionService RecordsManagementActionService instance + */ + public void setRecordsManagementActionService(RecordsManagementActionService rmActionService) + { + this.rmActionService = rmActionService; + } + + @Override + protected File executeTransfer(NodeRef transferNode, + WebScriptRequest req, WebScriptResponse res, + Status status, Cache cache) throws IOException + { + File report = null; + + // retrieve requested format + String format = req.getFormat(); + Map model = new HashMap(); + model.put("status", status); + model.put("cache", cache); + + try + { + // extract the destination parameter, ensure it's present and it is + // a record folder + JSONObject json = new JSONObject(new JSONTokener(req.getContent().getContent())); + if (!json.has(PARAM_DESTINATION)) + { + status.setCode(HttpServletResponse.SC_BAD_REQUEST, + "Mandatory '" + PARAM_DESTINATION + "' parameter has not been supplied"); + Map templateModel = createTemplateParameters(req, res, model); + sendStatus(req, res, status, cache, format, templateModel); + return null; + } + + String destinationParam = json.getString(PARAM_DESTINATION); + NodeRef destination = new NodeRef(destinationParam); + + if (!this.nodeService.exists(destination)) + { + status.setCode(HttpServletResponse.SC_NOT_FOUND, + "Node " + destination.toString() + " does not exist"); + Map templateModel = createTemplateParameters(req, res, model); + sendStatus(req, res, status, cache, format, templateModel); + return null; + } + + // ensure the node is a filePlan object + if (!RecordsManagementModel.TYPE_RECORD_FOLDER.equals(this.nodeService.getType(destination))) + { + status.setCode(HttpServletResponse.SC_BAD_REQUEST, + "Node " + destination.toString() + " is not a record folder"); + Map templateModel = createTemplateParameters(req, res, model); + sendStatus(req, res, status, cache, format, templateModel); + return null; + } + + if (logger.isDebugEnabled()) + logger.debug("Filing transfer report as record in record folder: " + destination); + + // generate the report (will be in JSON format) + report = generateHTMLTransferReport(transferNode); + + // file the report as a record + NodeRef record = fileTransferReport(report, destination); + + if (logger.isDebugEnabled()) + logger.debug("Filed transfer report as new record: " + record); + + // return success flag and record noderef as JSON + JSONObject responseJSON = new JSONObject(); + responseJSON.put(RESPONSE_SUCCESS, (record != null)); + if (record != null) + { + responseJSON.put(RESPONSE_RECORD, record.toString()); + responseJSON.put(RESPONSE_RECORD_NAME, + (String)nodeService.getProperty(record, ContentModel.PROP_NAME)); + } + + // setup response + String jsonString = responseJSON.toString(); + res.setContentType(MimetypeMap.MIMETYPE_JSON); + res.setContentEncoding("UTF-8"); + res.setHeader("Content-Length", Long.toString(jsonString.length())); + + // write the JSON response + res.getWriter().write(jsonString); + } + catch (JSONException je) + { + throw createStatusException(je, req, res); + } + + // return the file for deletion + return report; + } + + /** + * Generates a File containing the JSON representation of a transfer report. + * + * @param transferNode The transfer node + * @return File containing JSON representation of a transfer report + * @throws IOException + */ + File generateHTMLTransferReport(NodeRef transferNode) throws IOException + { + File report = TempFileProvider.createTempFile(REPORT_FILE_PREFIX, REPORT_FILE_SUFFIX); + Writer writer = null; + try + { + // get all 'transferred' nodes + NodeRef[] itemsToTransfer = getTransferNodes(transferNode); + + if (logger.isDebugEnabled()) + { + logger.debug("Generating HTML transfer report for " + itemsToTransfer.length + + " items into file: " + report.getAbsolutePath()); + } + + // create the writer + writer = new FileWriter(report); + + // use RMService to get disposition authority + String dispositionAuthority = null; + if (itemsToTransfer.length > 0) + { + // use the first transfer item to get to disposition schedule + DispositionSchedule ds = dispositionService.getDispositionSchedule(itemsToTransfer[0]); + if (ds != null) + { + dispositionAuthority = ds.getDispositionAuthority(); + } + } + + // write the HTML header + writer.write("\n"); + writer.write("\n\n"); + writer.write("Transfer Report\n"); + writer.write("\n"); + writer.write("\n

Transfer Report

\n"); + + writer.write(""); + writer.write(""); + writer.write(""); + writer.write(""); + writer.write("
Transfer Date:"); + Date transferDate = (Date)this.nodeService.getProperty(transferNode, ContentModel.PROP_CREATED); + writer.write(StringEscapeUtils.escapeHtml(transferDate.toString())); + writer.write("
Transfer Location:"); + writer.write(StringEscapeUtils.escapeHtml((String)this.nodeService.getProperty(transferNode, + RecordsManagementModel.PROP_TRANSFER_LOCATION))); + writer.write("
Performed By:"); + writer.write(StringEscapeUtils.escapeHtml((String)this.nodeService.getProperty(transferNode, + ContentModel.PROP_CREATOR))); + writer.write("
Disposition Authority:"); + writer.write(dispositionAuthority != null ? StringEscapeUtils.escapeHtml(dispositionAuthority) : ""); + writer.write("
\n"); + + writer.write("

Transferred Items

\n"); + + // write out HTML representation of items to transfer + generateTransferItemsHTML(writer, itemsToTransfer); + + // write the HTML footer + writer.write(""); + } + finally + { + if (writer != null) + { + try { writer.close(); } catch (IOException ioe) {} + } + } + + return report; + } + + /** + * Generates the JSON to represent the given NodeRefs + * + * @param writer Writer to write to + * @param itemsToTransfer NodeRefs being transferred + * @throws IOException + */ + protected void generateTransferItemsHTML(Writer writer, NodeRef[] itemsToTransfer) + throws IOException + { + for (NodeRef item : itemsToTransfer) + { + writer.write("
\n"); + if (ddService.isSubClass(nodeService.getType(item), ContentModel.TYPE_FOLDER)) + { + generateTransferFolderHTML(writer, item); + } + else + { + generateTransferRecordHTML(writer, item); + } + writer.write("
\n"); + } + } + + /** + * Generates the JSON to represent the given folder. + * + * @param writer Writer to write to + * @param folderNode Folder being transferred + * @throws IOException + */ + protected void generateTransferFolderHTML(Writer writer, NodeRef folderNode) + throws IOException + { + writer.write(""); + writer.write(StringEscapeUtils.escapeHtml((String)this.nodeService.getProperty(folderNode, + ContentModel.PROP_NAME))); + writer.write(" (Unique Folder Identifier: "); + writer.write(StringEscapeUtils.escapeHtml((String)this.nodeService.getProperty(folderNode, + RecordsManagementModel.PROP_IDENTIFIER))); + writer.write(")\n"); + + writer.write("
\n"); + + // NOTE: we don't expect any nested folder structures so just render + // the records contained in the folder. + + List assocs = this.nodeService.getChildAssocs(folderNode, + ContentModel.ASSOC_CONTAINS, RegexQNamePattern.MATCH_ALL); + for (ChildAssociationRef child : assocs) + { + NodeRef childRef = child.getChildRef(); + if (this.nodeService.hasAspect(childRef, RecordsManagementModel.ASPECT_RECORD)) + { + generateTransferRecordHTML(writer, childRef); + } + } + + writer.write("\n
\n"); + } + + /** + * Generates the JSON to represent the given record. + * + * @param writer Writer to write to + * @param recordNode Record being transferred + * @throws IOException + */ + protected void generateTransferRecordHTML(Writer writer, NodeRef recordNode) + throws IOException + { + writer.write("
\n"); + writer.write(" "); + writer.write(StringEscapeUtils.escapeHtml((String)this.nodeService.getProperty(recordNode, + ContentModel.PROP_NAME))); + writer.write(" (Unique Record Identifier: "); + writer.write(StringEscapeUtils.escapeHtml((String)this.nodeService.getProperty(recordNode, + RecordsManagementModel.PROP_IDENTIFIER))); + writer.write(")"); + + if (this.nodeService.hasAspect(recordNode, RecordsManagementModel.ASPECT_DECLARED_RECORD)) + { + Date declaredOn = (Date)this.nodeService.getProperty(recordNode, RecordsManagementModel.PROP_DECLARED_AT); + writer.write(" declared by "); + writer.write(StringEscapeUtils.escapeHtml((String)this.nodeService.getProperty(recordNode, + RecordsManagementModel.PROP_DECLARED_BY))); + writer.write(" on "); + writer.write(StringEscapeUtils.escapeHtml(declaredOn.toString())); + } + + writer.write("\n
\n"); + } + + /** + * Files the given transfer report as a record in the given record folder. + * + * @param report Report to file + * @param destination The destination record folder + * @return NodeRef of the created record + */ + protected NodeRef fileTransferReport(File report, NodeRef destination) + { + ParameterCheck.mandatory("report", report); + ParameterCheck.mandatory("destination", destination); + + NodeRef record = null; + + Map properties = new HashMap(1); + properties.put(ContentModel.PROP_NAME, report.getName()); + + // file the transfer report as an undeclared record + record = this.nodeService.createNode(destination, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, + QName.createValidLocalName(report.getName())), + ContentModel.TYPE_CONTENT, properties).getChildRef(); + + // Set the content + ContentWriter writer = this.contentService.getWriter(record, ContentModel.PROP_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_HTML); + writer.setEncoding("UTF-8"); + writer.putContent(report); + + // file the node as a record + this.rmActionService.executeRecordsManagementAction(record, FILE_ACTION); + + return record; + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/UserRightsReportGet.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/UserRightsReportGet.java new file mode 100644 index 0000000000..343a7d9e29 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/UserRightsReportGet.java @@ -0,0 +1,354 @@ +/* + * 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.module.org_alfresco_module_rm.script; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService; +import org.alfresco.module.org_alfresco_module_rm.security.Role; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.security.AuthorityService; +import org.alfresco.service.cmr.security.AuthorityType; +import org.alfresco.service.cmr.security.PersonService; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Implementation for Java backed webscript to return user rights report. + * + * @author Gavin Cornwell + */ +public class UserRightsReportGet extends DeclarativeWebScript +{ + protected AuthorityService authorityService; + protected PersonService personService; + protected NodeService nodeService; + protected RecordsManagementService rmService; + protected RecordsManagementSecurityService rmSecurityService; + + /** + * Sets the AuthorityService instance + * + * @param authorityService AuthorityService instance + */ + public void setAuthorityService(AuthorityService authorityService) + { + this.authorityService = authorityService; + } + + /** + * Sets the PersonService instance + * + * @param personService PersonService instance + */ + public void setPersonService(PersonService personService) + { + this.personService = personService; + } + + /** + * Sets the NodeService instance + * + * @param nodeService NodeService instance + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Sets the RecordsManagementService instance + * + * @param rmService The RecordsManagementService instance + */ + public void setRecordsManagementService(RecordsManagementService rmService) + { + this.rmService = rmService; + } + + /** + * Sets the RecordsManagementSecurityService instance + * + * @param rmSecurityService The RecordsManagementSecurityService instance + */ + public void setRecordsManagementSecurityService(RecordsManagementSecurityService rmSecurityService) + { + this.rmSecurityService = rmSecurityService; + } + + /* + * @see org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.Status, org.alfresco.web.scripts.Cache) + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + // get the RM root nodes in the system + List rmRoots = this.rmService.getFilePlans(); + + if (rmRoots == null || rmRoots.size() == 0) + { + status.setCode(HttpServletResponse.SC_BAD_REQUEST, + "There are no Records Management root nodes in the system"); + return null; + } + + // construct all the maps etc. needed to build the model + Map usersMap = new HashMap(8); + Map rolesMap = new HashMap(8); + Map groupsMap = new HashMap(8); + + // TODO: deal with presence of more than one root, for now we know it's only 1 + NodeRef rmRootNode = rmRoots.get(0); + + // iterate over all the roles for the file plan and construct models + Set roles = this.rmSecurityService.getRoles(rmRootNode); + for (Role role : roles) + { + // get or create the RoleModel object for current role + String roleName = role.getName(); + RoleModel roleModel = rolesMap.get(roleName); + if (roleModel == null) + { + roleModel = new RoleModel(role); + rolesMap.put(roleName, roleModel); + } + + // get the users for the current RM role + String group = role.getRoleGroupName(); + Set users = this.authorityService.getContainedAuthorities(AuthorityType.USER, group, true); + roleModel.setUsers(users); + + // setup a user model object for each user + for (String userName : users) + { + UserModel userModel = usersMap.get(userName); + if (userModel == null) + { + NodeRef userRef = this.personService.getPerson(userName); + userModel = new UserModel(userName, + (String)this.nodeService.getProperty(userRef, ContentModel.PROP_FIRSTNAME), + (String)this.nodeService.getProperty(userRef, ContentModel.PROP_LASTNAME)); + usersMap.put(userName, userModel); + } + + userModel.addRole(roleName); + } + + // get the groups for the cuurent RM role + Set groups = this.authorityService.getContainedAuthorities(AuthorityType.GROUP, group, false); + roleModel.setGroups(groups); + + // setup a user model object for each user in each group + for (String groupName : groups) + { + GroupModel groupModel = groupsMap.get(groupName); + if (groupModel == null) + { + groupModel = new GroupModel(groupName, + this.authorityService.getAuthorityDisplayName(groupName)); + groupsMap.put(groupName, groupModel); + } + + // get users in each group + Set groupUsers = this.authorityService.getContainedAuthorities(AuthorityType.USER, groupName, true); + for (String userName : groupUsers) + { + UserModel userModel = usersMap.get(userName); + if (userModel == null) + { + NodeRef userRef = this.personService.getPerson(userName); + userModel = new UserModel(userName, + (String)this.nodeService.getProperty(userRef, ContentModel.PROP_FIRSTNAME), + (String)this.nodeService.getProperty(userRef, ContentModel.PROP_LASTNAME)); + usersMap.put(userName, userModel); + } + + userModel.addGroup(groupName); + userModel.addRole(roleName); + groupModel.addUser(userName); + } + } + } + + // add all the lists data to a Map + Map reportModel = new HashMap(4); + reportModel.put("users", usersMap); + reportModel.put("roles", rolesMap); + reportModel.put("groups", groupsMap); + + // create model object with the lists model + Map model = new HashMap(1); + model.put("report", reportModel); + return model; + } + + /** + * Class to represent a role for use in a Freemarker template. + * + * @author Gavin Cornwell + */ + public class RoleModel extends Role + { + private Set users = new HashSet(8); + private Set groups = new HashSet(8); + + public RoleModel(Role role) + { + super(role.getName(), role.getDisplayLabel(), role.getCapabilities(), role.getRoleGroupName()); + } + + public void addUser(String username) + { + this.users.add(username); + } + + public void addGroup(String groupName) + { + this.groups.add(groupName); + } + + public void setUsers(Set users) + { + this.users = users; + } + + public void setGroups(Set groups) + { + this.groups = groups; + } + + public Set getUsers() + { + return this.users; + } + + public Set getGroups() + { + return this.groups; + } + } + + /** + * Class to represent a user for use in a Freemarker template. + * + * @author Gavin Cornwell + */ + public class UserModel + { + private String userName; + private String firstName; + private String lastName; + private Set roles; + private Set groups; + + public UserModel(String userName, String firstName, String lastName) + { + this.userName = userName; + this.firstName = firstName; + this.lastName = lastName; + this.roles = new HashSet(2); + this.groups = new HashSet(2); + } + + public String getUserName() + { + return this.userName; + } + + public String getFirstName() + { + return this.firstName; + } + + public String getLastName() + { + return this.lastName; + } + + public Set getRoles() + { + return this.roles; + } + + public Set getGroups() + { + return this.groups; + } + + public void addRole(String roleName) + { + this.roles.add(roleName); + } + + public void addGroup(String groupName) + { + this.groups.add(groupName); + } + } + + /** + * Class to represent a group for use in a Freemarker template. + * + * @author Gavin Cornwell + */ + public class GroupModel + { + private String name; + private String label; + private Set users; + + public GroupModel(String name, String label) + { + this.name = name; + this.label = label; + this.users = new HashSet(4); + } + + public String getName() + { + return this.name; + } + + public String getDisplayLabel() + { + return this.label; + } + + public Set getUsers() + { + return this.users; + } + + public void addUser(String userName) + { + this.users.add(userName); + } + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmEventDelete.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmEventDelete.java new file mode 100644 index 0000000000..e5d8b0501c --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmEventDelete.java @@ -0,0 +1,83 @@ +/* + * 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.module.org_alfresco_module_rm.script.admin; + +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEventService; +import org.springframework.extensions.webscripts.Cache; +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.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Records management event delete web script + * + * @author Roy Wetherall + */ +public class RmEventDelete extends DeclarativeWebScript +{ + @SuppressWarnings("unused") + private static Log logger = LogFactory.getLog(RmEventDelete.class); + + /** Reccords management event service */ + private RecordsManagementEventService rmEventService; + + /** + * Set the records management event service + * + * @param rmEventService + */ + public void setRecordsManagementEventService(RecordsManagementEventService rmEventService) + { + this.rmEventService = rmEventService; + } + + /** + * @see org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.Status, org.alfresco.web.scripts.Cache) + */ + @Override + public Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(); + + // Event name + Map templateVars = req.getServiceMatch().getTemplateVars(); + String eventName = templateVars.get("eventname"); + if (eventName == null) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "No event name was provided on the URL."); + } + + // Check the event exists + if (rmEventService.existsEvent(eventName) == false) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "The event " + eventName + " does not exist."); + } + + // Remove the event + rmEventService.removeEvent(eventName); + + return model; + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmEventGet.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmEventGet.java new file mode 100644 index 0000000000..ad01f08864 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmEventGet.java @@ -0,0 +1,82 @@ +/* + * 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.module.org_alfresco_module_rm.script.admin; + +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEvent; +import org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEventService; +import org.springframework.extensions.webscripts.Cache; +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.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Records management event GET web script + * + * @author Roy Wetherall + */ +public class RmEventGet extends DeclarativeWebScript +{ + @SuppressWarnings("unused") + private static Log logger = LogFactory.getLog(RmEventGet.class); + + /** Reccords management event service */ + private RecordsManagementEventService rmEventService; + + /** + * Set the records management event service + * + * @param rmEventService + */ + public void setRecordsManagementEventService(RecordsManagementEventService rmEventService) + { + this.rmEventService = rmEventService; + } + + @Override + public Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(); + + // Event name + Map templateVars = req.getServiceMatch().getTemplateVars(); + String eventName = templateVars.get("eventname"); + if (eventName == null) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "No event name was provided on the URL."); + } + + // Check the event exists + if (rmEventService.existsEvent(eventName) == false) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "The event " + eventName + " does not exist."); + } + + // Get the event + RecordsManagementEvent event = rmEventService.getEvent(eventName); + model.put("event", event); + + return model; + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmEventPut.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmEventPut.java new file mode 100644 index 0000000000..8717870eea --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmEventPut.java @@ -0,0 +1,122 @@ +/* + * 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.module.org_alfresco_module_rm.script.admin; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEvent; +import org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEventService; +import org.springframework.extensions.webscripts.Cache; +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.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; + +/** + * Records management event PUT web script + * + * @author Roy Wetherall + */ +public class RmEventPut extends DeclarativeWebScript +{ + @SuppressWarnings("unused") + private static Log logger = LogFactory.getLog(RmEventPut.class); + + /** Reccords management event service */ + private RecordsManagementEventService rmEventService; + + /** + * Set the records management event service + * + * @param rmEventService + */ + public void setRecordsManagementEventService(RecordsManagementEventService rmEventService) + { + this.rmEventService = rmEventService; + } + + @Override + public Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(); + JSONObject json = null; + try + { + // Event name + Map templateVars = req.getServiceMatch().getTemplateVars(); + String eventName = templateVars.get("eventname"); + if (eventName == null) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "No event name was provided on the URL."); + } + + // Check the event exists + if (rmEventService.existsEvent(eventName) == false) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "The event " + eventName + " does not exist."); + } + + json = new JSONObject(new JSONTokener(req.getContent().getContent())); + + String eventDisplayLabel = null; + if (json.has("eventDisplayLabel") == true) + { + eventDisplayLabel = json.getString("eventDisplayLabel"); + } + if (eventDisplayLabel == null || eventDisplayLabel.length() == 0) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "No event display label provided."); + } + + String eventType = null; + if (json.has("eventType") == true) + { + eventType = json.getString("eventType"); + } + if (eventType == null || eventType.length() == 0) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "No event type provided."); + } + + + RecordsManagementEvent event = rmEventService.addEvent(eventType, eventName, eventDisplayLabel); + model.put("event", event); + + } + catch (IOException iox) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Could not read content from req.", iox); + } + catch (JSONException je) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Could not parse JSON from req.", je); + } + + return model; + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmEventTypesGet.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmEventTypesGet.java new file mode 100644 index 0000000000..2b9a57a1f5 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmEventTypesGet.java @@ -0,0 +1,68 @@ +/* + * 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.module.org_alfresco_module_rm.script.admin; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEventService; +import org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEventType; +import org.springframework.extensions.webscripts.Cache; +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; + +/** + * Records management event types GET web script + * + * @author Roy Wetherall + */ +public class RmEventTypesGet extends DeclarativeWebScript +{ + @SuppressWarnings("unused") + private static Log logger = LogFactory.getLog(RmEventTypesGet.class); + + /** Reccords management event service */ + private RecordsManagementEventService rmEventService; + + /** + * Set the records management event service + * + * @param rmEventService + */ + public void setRecordsManagementEventService(RecordsManagementEventService rmEventService) + { + this.rmEventService = rmEventService; + } + + @Override + public Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(); + + // Get the events + List events = rmEventService.getEventTypes(); + model.put("eventtypes", events); + + return model; + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmEventsGet.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmEventsGet.java new file mode 100644 index 0000000000..91d0c96ef2 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmEventsGet.java @@ -0,0 +1,68 @@ +/* + * 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.module.org_alfresco_module_rm.script.admin; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEvent; +import org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEventService; +import org.springframework.extensions.webscripts.Cache; +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; + +/** + * Records management events GET web script + * + * @author Roy Wetherall + */ +public class RmEventsGet extends DeclarativeWebScript +{ + @SuppressWarnings("unused") + private static Log logger = LogFactory.getLog(RmEventsGet.class); + + /** Reccords management event service */ + private RecordsManagementEventService rmEventService; + + /** + * Set the records management event service + * + * @param rmEventService + */ + public void setRecordsManagementEventService(RecordsManagementEventService rmEventService) + { + this.rmEventService = rmEventService; + } + + @Override + public Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(); + + // Get the events + List events = rmEventService.getEvents(); + model.put("events", events); + + return model; + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmEventsPost.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmEventsPost.java new file mode 100644 index 0000000000..02aad56389 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmEventsPost.java @@ -0,0 +1,121 @@ +/* + * 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.module.org_alfresco_module_rm.script.admin; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEvent; +import org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEventService; +import org.alfresco.util.GUID; +import org.springframework.extensions.webscripts.Cache; +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.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; + +/** + * + * + * @author Roy Wetherall + */ +public class RmEventsPost extends DeclarativeWebScript +{ + @SuppressWarnings("unused") + private static Log logger = LogFactory.getLog(RmEventsPost.class); + + /** Reccords management event service */ + private RecordsManagementEventService rmEventService; + + /** + * Set the records management event service + * + * @param rmEventService + */ + public void setRecordsManagementEventService(RecordsManagementEventService rmEventService) + { + this.rmEventService = rmEventService; + } + + + @Override + public Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(); + JSONObject json = null; + try + { + json = new JSONObject(new JSONTokener(req.getContent().getContent())); + + String eventName = null; + if (json.has("eventName") == true) + { + eventName = json.getString("eventName"); + } + + if (eventName == null || eventName.length() == 0) + { + // Generate the event name + eventName = GUID.generate(); + } + + String eventDisplayLabel = null; + if (json.has("eventDisplayLabel") == true) + { + eventDisplayLabel = json.getString("eventDisplayLabel"); + } + if (eventDisplayLabel == null || eventDisplayLabel.length() == 0) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "No event display label provided."); + } + + String eventType = null; + if (json.has("eventType") == true) + { + eventType = json.getString("eventType"); + } + if (eventType == null || eventType.length() == 0) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "No event type provided."); + } + + RecordsManagementEvent event = rmEventService.addEvent(eventType, eventName, eventDisplayLabel); + model.put("event", event); + + } + catch (IOException iox) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Could not read content from req.", iox); + } + catch (JSONException je) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Could not parse JSON from req.", je); + } + + return model; + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmRoleDelete.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmRoleDelete.java new file mode 100644 index 0000000000..0f0fa50180 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmRoleDelete.java @@ -0,0 +1,85 @@ +/* + * 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.module.org_alfresco_module_rm.script.admin; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.springframework.extensions.webscripts.Cache; +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.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * + * + * @author Roy Wetherall + */ +public class RmRoleDelete extends DeclarativeWebScript +{ + @SuppressWarnings("unused") + private static Log logger = LogFactory.getLog(RmRoleDelete.class); + + private RecordsManagementService rmService; + private RecordsManagementSecurityService rmSecurityService; + + public void setRecordsManagementSecurityService(RecordsManagementSecurityService rmSecurityService) + { + this.rmSecurityService = rmSecurityService; + } + + public void setRecordsManagementService(RecordsManagementService rmService) + { + this.rmService = rmService; + } + + @Override + public Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(); + + // Role name + Map templateVars = req.getServiceMatch().getTemplateVars(); + String roleParam = templateVars.get("rolename"); + if (roleParam == null) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "No role name was provided on the URL."); + } + + List roots = rmService.getFilePlans(); + NodeRef root = roots.get(0); + + // Check that the role exists + if (rmSecurityService.existsRole(root, roleParam) == false) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "The role " + roleParam + " does not exist on the records managment root " + root); + } + + rmSecurityService.deleteRole(root, roleParam); + + return model; + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmRoleGet.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmRoleGet.java new file mode 100644 index 0000000000..85e008cf55 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmRoleGet.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.module.org_alfresco_module_rm.script.admin; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.springframework.extensions.webscripts.Cache; +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.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * + * + * @author Roy Wetherall + */ +public class RmRoleGet extends DeclarativeWebScript +{ + @SuppressWarnings("unused") + private static Log logger = LogFactory.getLog(RmRoleGet.class); + + private RecordsManagementService rmService; + private RecordsManagementSecurityService rmSecurityService; + + public void setRecordsManagementSecurityService(RecordsManagementSecurityService rmSecurityService) + { + this.rmSecurityService = rmSecurityService; + } + + public void setRecordsManagementService(RecordsManagementService rmService) + { + this.rmService = rmService; + } + + @Override + public Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(); + + // Role name + Map templateVars = req.getServiceMatch().getTemplateVars(); + String roleParam = templateVars.get("rolename"); + if (roleParam == null) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "No role name was provided on the URL."); + } + + // Get the root records management node + // TODO this should be passed + List roots = rmService.getFilePlans(); + NodeRef root = roots.get(0); + + // Check that the role exists + if (rmSecurityService.existsRole(root, roleParam) == false) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "The role " + roleParam + " does not exist on the records managment root " + root); + } + + model.put("role", rmSecurityService.getRole(root, roleParam)); + + return model; + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmRolePut.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmRolePut.java new file mode 100644 index 0000000000..11429ecf5d --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmRolePut.java @@ -0,0 +1,123 @@ +/* + * 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.module.org_alfresco_module_rm.script.admin; + +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.capability.Capability; +import org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService; +import org.alfresco.module.org_alfresco_module_rm.security.Role; +import org.alfresco.service.cmr.repository.NodeRef; +import org.springframework.extensions.webscripts.Cache; +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.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; + +/** + * + * + * @author Roy Wetherall + */ +public class RmRolePut extends DeclarativeWebScript +{ + @SuppressWarnings("unused") + private static Log logger = LogFactory.getLog(RmRolePut.class); + + private RecordsManagementService rmService; + private RecordsManagementSecurityService rmSecurityService; + + public void setRecordsManagementSecurityService(RecordsManagementSecurityService rmSecurityService) + { + this.rmSecurityService = rmSecurityService; + } + + public void setRecordsManagementService(RecordsManagementService rmService) + { + this.rmService = rmService; + } + + @Override + public Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(); + JSONObject json = null; + try + { + // Role name + Map templateVars = req.getServiceMatch().getTemplateVars(); + String roleParam = templateVars.get("rolename"); + if (roleParam == null) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "No role name was provided on the URL."); + } + + json = new JSONObject(new JSONTokener(req.getContent().getContent())); + String name = json.getString("name"); + // TODO check + String displayLabel = json.getString("displayLabel"); + // TODO check + + JSONArray capabilitiesArray = json.getJSONArray("capabilities"); + Set capabilites = new HashSet(capabilitiesArray.length()); + for (int i = 0; i < capabilitiesArray.length(); i++) + { + Capability capability = rmSecurityService.getCapability(capabilitiesArray.getString(i)); + capabilites.add(capability); + } + + List roots = rmService.getFilePlans(); + NodeRef root = roots.get(0); + + // Check that the role exists + if (rmSecurityService.existsRole(root, roleParam) == false) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "The role " + roleParam + " does not exist on the records managment root " + root); + } + + Role role = rmSecurityService.updateRole(root, name, displayLabel, capabilites); + model.put("role", role); + + } + catch (IOException iox) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Could not read content from req.", iox); + } + catch (JSONException je) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Could not parse JSON from req.", je); + } + + return model; + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmRolesGet.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmRolesGet.java new file mode 100644 index 0000000000..76d108c124 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmRolesGet.java @@ -0,0 +1,85 @@ +/* + * 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.module.org_alfresco_module_rm.script.admin; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService; +import org.alfresco.module.org_alfresco_module_rm.security.Role; +import org.alfresco.service.cmr.repository.NodeRef; +import org.springframework.extensions.webscripts.Cache; +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; + +/** + * + * + * @author Roy Wetherall + */ +public class RmRolesGet extends DeclarativeWebScript +{ + @SuppressWarnings("unused") + private static Log logger = LogFactory.getLog(RmRolesGet.class); + + private RecordsManagementService rmService; + private RecordsManagementSecurityService rmSecurityService; + + public void setRecordsManagementSecurityService(RecordsManagementSecurityService rmSecurityService) + { + this.rmSecurityService = rmSecurityService; + } + + public void setRecordsManagementService(RecordsManagementService rmService) + { + this.rmService = rmService; + } + + @Override + public Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(); + Set roles = null; + + // TODO should be passed + List roots = rmService.getFilePlans(); + NodeRef root = roots.get(0); + + // Get the user filter + String user = req.getParameter("user"); + if (user != null && user.length() != 0) + { + roles = rmSecurityService.getRolesByUser(root, user); + } + else + { + roles = rmSecurityService.getRoles(root); + } + + model.put("roles", roles); + + return model; + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmRolesPost.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmRolesPost.java new file mode 100644 index 0000000000..fb3656aebd --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/admin/RmRolesPost.java @@ -0,0 +1,111 @@ +/* + * 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.module.org_alfresco_module_rm.script.admin; + +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.capability.Capability; +import org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService; +import org.alfresco.module.org_alfresco_module_rm.security.Role; +import org.alfresco.service.cmr.repository.NodeRef; +import org.springframework.extensions.webscripts.Cache; +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.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; + +/** + * + * + * @author Roy Wetherall + */ +public class RmRolesPost extends DeclarativeWebScript +{ + @SuppressWarnings("unused") + private static Log logger = LogFactory.getLog(RmRolesPost.class); + + private RecordsManagementService rmService; + private RecordsManagementSecurityService rmSecurityService; + + public void setRecordsManagementSecurityService(RecordsManagementSecurityService rmSecurityService) + { + this.rmSecurityService = rmSecurityService; + } + + public void setRecordsManagementService(RecordsManagementService rmService) + { + this.rmService = rmService; + } + + @Override + public Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map model = new HashMap(); + JSONObject json = null; + try + { + json = new JSONObject(new JSONTokener(req.getContent().getContent())); + String name = json.getString("name"); + // TODO check + String displayString = json.getString("displayLabel"); + // TODO check + + JSONArray capabilitiesArray = json.getJSONArray("capabilities"); + Set capabilites = new HashSet(capabilitiesArray.length()); + for (int i = 0; i < capabilitiesArray.length(); i++) + { + Capability capability = rmSecurityService.getCapability(capabilitiesArray.getString(i)); + capabilites.add(capability); + } + + List roots = rmService.getFilePlans(); + NodeRef root = roots.get(0); + + Role role = rmSecurityService.createRole(root, name, displayString, capabilites); + + Set roles = rmSecurityService.getRoles(root); + model.put("role", role); + + } + catch (IOException iox) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Could not read content from req.", iox); + } + catch (JSONException je) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Could not parse JSON from req.", je); + } + + return model; + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/slingshot/RMSavedSearchesDelete.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/slingshot/RMSavedSearchesDelete.java new file mode 100644 index 0000000000..e0022bf527 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/slingshot/RMSavedSearchesDelete.java @@ -0,0 +1,95 @@ +/* + * 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.module.org_alfresco_module_rm.script.slingshot; + +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.search.RecordsManagementSearchService; +import org.alfresco.service.cmr.site.SiteService; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Records Management saved search DELETE web script + * + * @author Roy Wetherall + */ +public class RMSavedSearchesDelete extends DeclarativeWebScript +{ + /** Records management search service */ + protected RecordsManagementSearchService recordsManagementSearchService; + + /** Site service */ + protected SiteService siteService; + + /** + * @param recordsManagementSearchService records management search service + */ + public void setRecordsManagementSearchService(RecordsManagementSearchService recordsManagementSearchService) + { + this.recordsManagementSearchService = recordsManagementSearchService; + } + + /** + * @param siteService site service + */ + public void setSiteService(SiteService siteService) + { + this.siteService = siteService; + } + + /* + * @see org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.Status, org.alfresco.web.scripts.Cache) + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + Map templateVars = req.getServiceMatch().getTemplateVars(); + + // Get the site id and confirm it's valid + String siteId = templateVars.get("site"); + if (siteId == null || siteId.length() == 0) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Site id not provided."); + } + if (siteService.getSite(siteId) == null) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "Site not found."); + } + + // Get the name of the saved search + String name = templateVars.get("name"); + if (name == null || name.length() == 0) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Saved search name not provided."); + } + + // Delete the saved search + recordsManagementSearchService.deleteSavedSearch(siteId, name); + + // Indicate success in the model + Map model = new HashMap(1); + model.put("success", true); + return model; + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/slingshot/RMSavedSearchesGet.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/slingshot/RMSavedSearchesGet.java new file mode 100644 index 0000000000..aee41dcb28 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/slingshot/RMSavedSearchesGet.java @@ -0,0 +1,157 @@ +/* + * 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.module.org_alfresco_module_rm.script.slingshot; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.search.RecordsManagementSearchService; +import org.alfresco.module.org_alfresco_module_rm.search.SavedSearchDetails; +import org.alfresco.service.cmr.site.SiteService; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * RM saved searches GET web script + * + * @author Roy Wetherall + */ +public class RMSavedSearchesGet extends DeclarativeWebScript +{ + /** Records management search service */ + protected RecordsManagementSearchService recordsManagementSearchService; + + /** Site service */ + protected SiteService siteService; + + /** + * @param recordsManagementSearchService records management search service + */ + public void setRecordsManagementSearchService(RecordsManagementSearchService recordsManagementSearchService) + { + this.recordsManagementSearchService = recordsManagementSearchService; + } + + /** + * @param siteService site service + */ + public void setSiteService(SiteService siteService) + { + this.siteService = siteService; + } + + /* + * @see org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.Status, org.alfresco.web.scripts.Cache) + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + // create model object with the lists model + Map model = new HashMap(13); + + // Get the site id and confirm it is valid + Map templateVars = req.getServiceMatch().getTemplateVars(); + String siteId = templateVars.get("site"); + if (siteId == null || siteId.length() == 0) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Site id not provided."); + } + if (siteService.getSite(siteId) == null) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "Site not found."); + } + + // TODO determine whether this is still relevant +// String isPublicString = req.getParameter("p"); +// boolean isPublic = false; +// if (isPublicString != null && isPublicString.length() != 0) +// { +// isPublic = Boolean.parseBoolean(isPublicString); +// } + + // Get the saved search details + List details = recordsManagementSearchService.getSavedSearches(siteId); + List items = new ArrayList(); + for (SavedSearchDetails savedSearchDetails : details) + { + String name = savedSearchDetails.getName(); + String description = savedSearchDetails.getDescription(); + String query = savedSearchDetails.getCompatibility().getQuery(); + String params = savedSearchDetails.getCompatibility().getParams(); + String sort = savedSearchDetails.getCompatibility().getSort(); + + Item item = new Item(name, description, query, params, sort); + items.add(item); + } + + model.put("savedSearches", items); + return model; + } + + /** + * Item class to contain information about items being placed in model. + */ + public class Item + { + private String name; + private String description; + private String query; + private String params; + private String sort; + + public Item(String name, String description, String query, String params, String sort) + { + this.name = name; + this.description = description; + this.query = query; + this.params = params; + this.sort = sort; + } + + public String getName() + { + return name; + } + + public String getDescription() + { + return description; + } + + public String getQuery() + { + return query; + } + + public String getParams() + { + return params; + } + + public String getSort() + { + return sort; + } + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/slingshot/RMSavedSearchesPost.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/slingshot/RMSavedSearchesPost.java new file mode 100644 index 0000000000..2d36073c58 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/slingshot/RMSavedSearchesPost.java @@ -0,0 +1,169 @@ +/* + * 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.module.org_alfresco_module_rm.script.slingshot; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.search.RecordsManagementSearchParameters; +import org.alfresco.module.org_alfresco_module_rm.search.RecordsManagementSearchService; +import org.alfresco.module.org_alfresco_module_rm.search.SavedSearchDetailsCompatibility; +import org.alfresco.service.cmr.site.SiteService; +import org.alfresco.service.namespace.NamespaceService; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * Records management saved search POST web script. + * + * @author Roy Wetherall + */ +public class RMSavedSearchesPost extends DeclarativeWebScript +{ + /** Records management search service */ + protected RecordsManagementSearchService recordsManagementSearchService; + + /** Site service */ + protected SiteService siteService; + + /** Namespace service */ + protected NamespaceService namespaceService; + + /** + * @param recordsManagementSearchService records management search service + */ + public void setRecordsManagementSearchService(RecordsManagementSearchService recordsManagementSearchService) + { + this.recordsManagementSearchService = recordsManagementSearchService; + } + + /** + * @param siteService site service + */ + public void setSiteService(SiteService siteService) + { + this.siteService = siteService; + } + + /** + * @param namespaceService namespace service + */ + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + /** + * @see org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.Status, org.alfresco.web.scripts.Cache) + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + // Get the site id and confirm it is valid + Map templateVars = req.getServiceMatch().getTemplateVars(); + String siteId = templateVars.get("site"); + if (siteId == null || siteId.length() == 0) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Site id not provided."); + } + if (siteService.getSite(siteId) == null) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "Site not found."); + } + + // Example format of posted Saved Search JSON: + // { + // "name": "search name", + // "description": "the search description", + // "query": "the complete search query string", + // "public": boolean, + // "params": "terms=keywords:xyz&undeclared=true", + // "sort": "cm:name/asc" + //} + + try + { + // Parse the JSON passed in the request + JSONObject json = new JSONObject(new JSONTokener(req.getContent().getContent())); + + // Get the details of the saved search + if (json.has("name") == false) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Mandatory 'name' parameter was not provided in request body"); + } + String name = json.getString("name"); + String description = null; + if (json.has("description") == true) + { + description = json.getString("description"); + } + boolean isPublic = true; + if (json.has("public") == true) + { + isPublic = json.getBoolean("public"); + } + // NOTE: we do not need to worry about the query + if (json.has("params") == false) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Mandatory 'params' parameter was not provided in request body"); + } + String params = json.getString("params"); + String sort = null; + if (json.has("sort") == true) + { + sort = json.getString("sort"); + } + + // Use the compatibility class to create a saved search details and save + String search = SavedSearchDetailsCompatibility.getSearchFromParams(params); + if (search == null) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Mandatory 'terms' was not provided in 'params' parameter found in the request body"); + } + RecordsManagementSearchParameters searchParameters = SavedSearchDetailsCompatibility.createSearchParameters(params, sort, namespaceService); + recordsManagementSearchService.saveSearch(siteId, name, description, search, searchParameters, isPublic); + + } + catch (IOException iox) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Could not read content from req.", iox); + } + catch (JSONException je) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, + "Could not parse JSON from req.", je); + } + + // Indicate success in the model + Map model = new HashMap(1); + model.put("success", true); + return model; + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/slingshot/RMSearchGet.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/slingshot/RMSearchGet.java new file mode 100644 index 0000000000..ef5a3face2 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/slingshot/RMSearchGet.java @@ -0,0 +1,445 @@ +/* + * 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.module.org_alfresco_module_rm.script.slingshot; + +import java.io.Serializable; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.module.org_alfresco_module_rm.search.RecordsManagementSearchParameters; +import org.alfresco.module.org_alfresco_module_rm.search.RecordsManagementSearchService; +import org.alfresco.module.org_alfresco_module_rm.search.SavedSearchDetailsCompatibility; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.cmr.site.SiteService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.apache.commons.lang.ArrayUtils; +import org.apache.commons.lang.StringUtils; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * RM search GET web script + * + * @author Roy Wetherall + */ +public class RMSearchGet extends DeclarativeWebScript +{ + /** URL Parameters */ + private static final String PARAM_QUERY = "query"; + private static final String PARAM_SORTBY = "sortby"; + private static final String PARAM_FILTERS = "filters"; + private static final String PARAM_MAX_ITEMS = "maxitems"; + + /** Records management search service */ + protected RecordsManagementSearchService recordsManagementSearchService; + + /** Site service */ + protected SiteService siteService; + + /** Namespace service */ + protected NamespaceService namespaceService; + + /** Node serivce */ + protected NodeService nodeService; + + /** Dictionary service */ + protected DictionaryService dictionaryService; + + /** Permission service */ + protected PermissionService permissionService; + + /** Person service */ + protected PersonService personService; + + /** Content service */ + protected ContentService contentService; + + /** Person data cache */ + private Map personDataCache = null; + + /** + * @param recordsManagementSearchService records management search service + */ + public void setRecordsManagementSearchService(RecordsManagementSearchService recordsManagementSearchService) + { + this.recordsManagementSearchService = recordsManagementSearchService; + } + + /** + * @param siteService site service + */ + public void setSiteService(SiteService siteService) + { + this.siteService = siteService; + } + + /** + * @param namespaceService namespace service + */ + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + /** + * @param nodeService node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * @param dictionaryService dictionary service + */ + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + /** + * @param permissionService permission service + */ + public void setPermissionService(PermissionService permissionService) + { + this.permissionService = permissionService; + } + + /** + * @param personService person service + */ + public void setPersonService(PersonService personService) + { + this.personService = personService; + } + + /** + * @param contentService content service + */ + public void setContentService(ContentService contentService) + { + this.contentService = contentService; + } + + /* + * @see org.alfresco.web.scripts.DeclarativeWebScript#executeImpl(org.alfresco.web.scripts.WebScriptRequest, org.alfresco.web.scripts.Status, org.alfresco.web.scripts.Cache) + */ + @Override + protected Map executeImpl(WebScriptRequest req, Status status, Cache cache) + { + // Get the site id and confirm it is valid + Map templateVars = req.getServiceMatch().getTemplateVars(); + String siteId = templateVars.get("site"); + if (siteId == null || siteId.length() == 0) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Site id not provided."); + } + if (siteService.getSite(siteId) == null) + { + throw new WebScriptException(Status.STATUS_NOT_FOUND, "Site not found."); + } + + // Get the query parameter + String query = req.getParameter(PARAM_QUERY); + // TODO check that this is there + + String sortby = req.getParameter(PARAM_SORTBY); + // TODO this is optional + + String filters = req.getParameter(PARAM_FILTERS); + // TODO this is optional + + // Convert into a rm search parameter object + RecordsManagementSearchParameters searchParameters = + SavedSearchDetailsCompatibility.createSearchParameters(filters, new String[]{",", "/"}, sortby, namespaceService); + + // Set the max results + String maxItems = req.getParameter(PARAM_MAX_ITEMS); + if (maxItems != null && maxItems.length() != 0) + { + searchParameters.setMaxItems(Integer.parseInt(maxItems)); + } + + // Execute search + List results = recordsManagementSearchService.search(siteId, query, searchParameters); + + // Reset person data cache + personDataCache = new HashMap(57); + + // Process the result items + Item[] items = new Item[results.size()]; + int index = 0; + for (NodeRef nodeRef : results) + { + items[index] = new Item(nodeRef); + index++; + } + + // Return model + Map model = new HashMap(1); + model.put("items", items); + return model; + + } + + /** + * Item class to contain information about items being placed in model. + */ + public class Item + { + private NodeRef nodeRef; + private String type; + private int size; + private String parentFolder = ""; + private String browseUrl; + private boolean isContainer; + private String modifiedBy; + private String createdBy; + private Map nodeProperties; + private Map properties; + + public Item(NodeRef nodeRef) + { + // Set node ref + this.nodeRef = nodeRef; + + // Get type + QName nodeRefType = nodeService.getType(nodeRef); + this.type = nodeRefType.toPrefixString(namespaceService); + + // Get properties + this.nodeProperties = nodeService.getProperties(nodeRef); + + // Determine if container or not + isContainer = true; + if (dictionaryService.isSubClass(nodeRefType, ContentModel.TYPE_CONTENT) == true) + { + isContainer = false; + } + + // Get parent node reference + NodeRef parent = null; + ChildAssociationRef assoc = nodeService.getPrimaryParent(nodeRef); + if (assoc != null) + { + parent = assoc.getParentRef(); + } + + if (isContainer == true) + { + this.size = -1; + + String displayPath = nodeService.getPath(nodeRef).toDisplayPath(nodeService, permissionService); + String[] pathElements = displayPath.split("/"); + if (pathElements.length >= 5) + { + if (pathElements.length > 5) + { + this.parentFolder = (String)nodeService.getProperty(parent, ContentModel.PROP_NAME); + } + + pathElements = (String[])ArrayUtils.subarray(pathElements, 5, pathElements.length); + String newPath = StringUtils.join(pathElements, "/"); + StringBuilder relPath = new StringBuilder("/").append(newPath); + if (relPath.length() > 1) + { + relPath.append("/"); + } + relPath.append(getName()); + try + { + this.browseUrl = "documentlibrary?path=" + URLEncoder.encode(relPath.toString(), "UTF-8"); + } + catch (UnsupportedEncodingException e) + { + throw new AlfrescoRuntimeException("Could not process search results.", e); + } + } + } + else + { + // Get the document size + ContentData contentData = (ContentData)nodeProperties.get(ContentModel.PROP_CONTENT); + this.size = 0; + if (contentData != null) + { + this.size = (int)contentData.getSize(); + } + + // Set the document parent name + if (parent != null) + { + this.parentFolder = (String)nodeService.getProperty(parent, ContentModel.PROP_NAME); + } + + // Set the document browse URL + this.browseUrl = "document-details?nodeRef=" + nodeRef.toString(); + } + + this.modifiedBy = getDisplayName(getModifiedByUser()); + this.createdBy = getDisplayName(getCreatedByUser()); + + // Process the custom properties + properties = new HashMap(nodeProperties.size()); + for (Map.Entry entry : nodeProperties.entrySet()) + { + QName qName = entry.getKey().getPrefixedQName(namespaceService); + if (RecordsManagementModel.RM_URI.equals(qName.getNamespaceURI()) == true || + RecordsManagementModel.RM_CUSTOM_URI.equals(qName.getNamespaceURI()) == true) + { + String prefixName = qName.getPrefixString().replace(":", "_"); + Serializable value = entry.getValue(); + if (value instanceof NodeRef) + { + value = value.toString(); + } + else if (value instanceof ContentData) + { + ContentReader contentReader = contentService.getReader(nodeRef, qName); + value = contentReader.getContentString(); + } + properties.put(prefixName, entry.getValue()); + } + } + } + + private String getDisplayName(String userName) + { + String result = personDataCache.get(userName); + if (result == null) + { + NodeRef person = personService.getPerson(userName); + if (person != null) + { + StringBuffer displayName = new StringBuffer(128); + displayName.append(nodeService.getProperty(person, ContentModel.PROP_FIRSTNAME)) + .append(" ") + .append(nodeService.getProperty(person, ContentModel.PROP_LASTNAME)); + result = displayName.toString(); + } + else + { + result = userName; + } + personDataCache.put(userName, result); + } + + return result; + } + + public NodeRef getNodeRef() + { + return nodeRef; + } + + public String getType() + { + return type; + } + + public String getName() + { + return (String)nodeProperties.get(ContentModel.PROP_NAME); + } + + public String getTitle() + { + return (String)nodeProperties.get(ContentModel.PROP_TITLE); + } + + public String getDescription() + { + return (String)nodeProperties.get(ContentModel.PROP_DESCRIPTION); + } + + public Date getModifiedOn() + { + return (Date)nodeProperties.get(ContentModel.PROP_MODIFIED); + } + + public String getModifiedByUser() + { + return (String)nodeProperties.get(ContentModel.PROP_MODIFIER); + } + + public String getModifiedBy() + { + return modifiedBy; + } + + public Date getCreatedOn() + { + return (Date)nodeProperties.get(ContentModel.PROP_CREATED); + } + + public String getCreatedByUser() + { + return (String)nodeProperties.get(ContentModel.PROP_CREATOR); + } + + public String getCreatedBy() + { + return createdBy; + } + + public String getAuthor() + { + return (String)nodeProperties.get(ContentModel.PROP_AUTHOR); + } + + public String getParentFolder() + { + return parentFolder; + } + + public int getSize() + { + return size; + } + + public String getBrowseUrl() + { + return browseUrl; + } + + public Map getProperties() + { + return properties; + } + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/search/RecordsManagementSearchParameters.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/search/RecordsManagementSearchParameters.java new file mode 100644 index 0000000000..4529833148 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/search/RecordsManagementSearchParameters.java @@ -0,0 +1,370 @@ +/* + * 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.module.org_alfresco_module_rm.search; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * @author Roy Wetherall + */ +@SuppressWarnings("serial") +public class RecordsManagementSearchParameters +{ + /** Default sort order */ + private static final Map DEFAULT_SORT_ORDER = new HashMap() + { + { + put(ContentModel.PROP_NAME, Boolean.TRUE); + } + }; + + /** Default templates */ + private static final Map DEFAULT_TEMPLATES = new HashMap() + { + { + put("keywords", "%(cm:name cm:title cm:description TEXT)"); + put("name", "%(cm:name)"); + put("title", "%(cm:title)"); + put("description", "%(cm:description)"); + put("creator", "%(cm:creator)"); + put("created", "%(cm:created)"); + put("modifier", "%(cm:modifier)"); + put("modified", "%(cm:modified)"); + put("author", "%(cm:author)"); + put("markings", "%(rmc:supplementalMarkingList)"); + put("dispositionEvents", "%(rma:recordSearchDispositionEvents)"); + put("dispositionActionName", "%(rma:recordSearchDispositionActionName)"); + put("dispositionActionAsOf", "%(rma:recordSearchDispositionActionAsOf)"); + put("dispositionEventsEligible", "%(rma:recordSearchDispositionEventsEligible)"); + put("dispositionPeriod", "%(rma:recordSearchDispositionPeriod)"); + put("hasDispositionSchedule", "%(rma:recordSearchHasDispositionSchedule)"); + put("dispositionInstructions", "%(rma:recordSearchDispositionInstructions)"); + put("dispositionAuthority", "%(rma:recordSearchDispositionAuthority)"); + put("holdReason", "%(rma:recordSearchHoldReason)"); + put("vitalRecordReviewPeriod", "%(rma:recordSearchVitalRecordReviewPeriod)"); + } + }; + + /** Default included container types */ + private static final List DEFAULT_INCLUDED_CONTAINER_TYPES = Collections.emptyList(); + + /** Max items */ + private int maxItems = 500; + + private boolean includeRecords = true; + private boolean includeUndeclaredRecords = false; + private boolean includeVitalRecords = false; + private boolean includeRecordFolders = true; + private boolean includeFrozen = false; + private boolean includeCutoff = false; + + private List includedContainerTypes = DEFAULT_INCLUDED_CONTAINER_TYPES; + private Map sortOrder = DEFAULT_SORT_ORDER; + private Map templates = DEFAULT_TEMPLATES; + + private static final String JSON_MAXITEMS = "maxitems"; + private static final String JSON_RECORDS = "records"; + private static final String JSON_UNDECLAREDRECORDS = "undeclaredrecords"; + private static final String JSON_VITALRECORDS = "vitalrecords"; + private static final String JSON_RECORDFOLDERES = "recordfolders"; + private static final String JSON_FROZEN = "frozen"; + private static final String JSON_CUTOFF = "cutoff"; + private static final String JSON_CONTAINERTYPES = "containertypes"; + private static final String JSON_SORT = "sort"; + private static final String JSON_FIELD = "field"; + private static final String JSON_ASCENDING = "ascending"; + + /** + * { + * "maxItems" : 500, + * "records" : true, + * "undeclaredrecords" : false, + * "vitalrecords" : false, + * "recordfolders" : false, + * "frozen" : false, + * "cutoff" : false, + * "containertypes" : + * [ + * "rma:recordSeries", + * "rma:recordCategory" + * ] + * "sort" : + * [ + * { + * "field" : "cm:name", + * "ascending" : true + * } + * ] + * } + */ + public static RecordsManagementSearchParameters createFromJSON(String json, NamespaceService namespaceService) + { + try + { + JSONObject jsonObject = new JSONObject(json); + return RecordsManagementSearchParameters.createFromJSON(jsonObject, namespaceService); + } + catch (JSONException e) + { + throw new AlfrescoRuntimeException("Unable to create records management search parameters from json string. " + json, e); + } + } + + /** + * + * @param jsonObject + * @return + */ + public static RecordsManagementSearchParameters createFromJSON(JSONObject jsonObject, NamespaceService namespaceService) + { + try + { + RecordsManagementSearchParameters searchParameters = new RecordsManagementSearchParameters(); + + // Get the search parameter properties + if (jsonObject.has(JSON_MAXITEMS) == true) + { + searchParameters.setMaxItems(jsonObject.getInt(JSON_MAXITEMS)); + } + if (jsonObject.has(JSON_RECORDS) == true) + { + searchParameters.setIncludeRecords(jsonObject.getBoolean(JSON_RECORDS)); + } + if (jsonObject.has(JSON_UNDECLAREDRECORDS) == true) + { + searchParameters.setIncludeUndeclaredRecords(jsonObject.getBoolean(JSON_UNDECLAREDRECORDS)); + } + if (jsonObject.has(JSON_VITALRECORDS) == true) + { + searchParameters.setIncludeVitalRecords(jsonObject.getBoolean(JSON_VITALRECORDS)); + } + if (jsonObject.has(JSON_RECORDFOLDERES) == true) + { + searchParameters.setIncludeRecordFolders(jsonObject.getBoolean(JSON_RECORDFOLDERES)); + } + if (jsonObject.has(JSON_FROZEN) == true) + { + searchParameters.setIncludeFrozen(jsonObject.getBoolean(JSON_FROZEN)); + } + if (jsonObject.has(JSON_CUTOFF) == true) + { + searchParameters.setIncludeCutoff(jsonObject.getBoolean(JSON_CUTOFF)); + } + + // Get container types + if (jsonObject.has(JSON_CONTAINERTYPES) == true) + { + JSONArray jsonArray = jsonObject.getJSONArray(JSON_CONTAINERTYPES); + List containerTypes = new ArrayList(jsonArray.length()); + for (int i = 0; i < jsonArray.length(); i++) + { + String type = jsonArray.getString(i); + containerTypes.add(QName.createQName(type, namespaceService)); + } + searchParameters.setIncludedContainerTypes(containerTypes); + } + + // Get sort details + if (jsonObject.has(JSON_SORT) == true) + { + JSONArray jsonArray = jsonObject.getJSONArray(JSON_SORT); + Map sortOrder = new HashMap(jsonArray.length()); + for (int i = 0; i < jsonArray.length(); i++) + { + JSONObject sortJSONObject = jsonArray.getJSONObject(i); + if (sortJSONObject.has(JSON_FIELD) == true && + sortJSONObject.has(JSON_ASCENDING) == true) + { + sortOrder.put( + QName.createQName(sortJSONObject.getString(JSON_FIELD), namespaceService), + Boolean.valueOf(sortJSONObject.getBoolean(JSON_ASCENDING))); + } + } + searchParameters.setSortOrder(sortOrder); + } + + return searchParameters; + } + catch (JSONException e) + { + throw new AlfrescoRuntimeException("Unable to create records management search parameters from json string. " + jsonObject.toString(), e); + } + } + + /** + * + * @return + */ + public String toJSONString(NamespaceService namespaceService) + { + return toJSONObject(namespaceService).toString(); + } + + public JSONObject toJSONObject(NamespaceService namespaceService) + { + try + { + JSONObject jsonObject = new JSONObject(); + jsonObject.put(JSON_MAXITEMS, maxItems); + jsonObject.put(JSON_RECORDS, includeRecords); + jsonObject.put(JSON_UNDECLAREDRECORDS, includeUndeclaredRecords); + jsonObject.put(JSON_VITALRECORDS, includeVitalRecords); + jsonObject.put(JSON_RECORDFOLDERES, includeRecordFolders); + jsonObject.put(JSON_FROZEN, includeFrozen); + jsonObject.put(JSON_CUTOFF, includeCutoff); + + // Included containers + JSONArray jsonArray = new JSONArray(); + for (QName containerType : includedContainerTypes) + { + jsonArray.put(containerType.toPrefixString(namespaceService)); + } + jsonObject.put(JSON_CONTAINERTYPES, jsonArray); + + // Sort + JSONArray jsonSortArray = new JSONArray(); + for (Map.Entry entry : sortOrder.entrySet()) + { + JSONObject jsonEntry = new JSONObject(); + jsonEntry.put(JSON_FIELD, entry.getKey().toPrefixString(namespaceService)); + jsonEntry.put(JSON_ASCENDING, entry.getValue().booleanValue()); + jsonSortArray.put(jsonEntry); + } + jsonObject.put(JSON_SORT, jsonSortArray); + + return jsonObject; + } + catch (JSONException e) + { + throw new AlfrescoRuntimeException("Unable to generate json string for records management search parameters.", e); + } + } + + public void setMaxItems(int maxItems) + { + this.maxItems = maxItems; + } + + public int getMaxItems() + { + return maxItems; + } + + public void setSortOrder(Map sortOrder) + { + this.sortOrder = sortOrder; + } + + public Map getSortOrder() + { + return sortOrder; + } + + public void setTemplates(Map templates) + { + this.templates = templates; + } + + public Map getTemplates() + { + return templates; + } + + public void setIncludeRecords(boolean includeRecords) + { + this.includeRecords = includeRecords; + } + + public boolean isIncludeRecords() + { + return includeRecords; + } + + public void setIncludeUndeclaredRecords(boolean includeUndeclaredRecords) + { + this.includeUndeclaredRecords = includeUndeclaredRecords; + } + + public boolean isIncludeUndeclaredRecords() + { + return includeUndeclaredRecords; + } + + public void setIncludeVitalRecords(boolean includeVitalRecords) + { + this.includeVitalRecords = includeVitalRecords; + } + + public boolean isIncludeVitalRecords() + { + return includeVitalRecords; + } + + public void setIncludeRecordFolders(boolean includeRecordFolders) + { + this.includeRecordFolders = includeRecordFolders; + } + + public boolean isIncludeRecordFolders() + { + return includeRecordFolders; + } + + public void setIncludeFrozen(boolean includeFrozen) + { + this.includeFrozen = includeFrozen; + } + + public boolean isIncludeFrozen() + { + return includeFrozen; + } + + public void setIncludeCutoff(boolean includeCutoff) + { + this.includeCutoff = includeCutoff; + } + + public boolean isIncludeCutoff() + { + return includeCutoff; + } + + public void setIncludedContainerTypes(List includedContainerTypes) + { + this.includedContainerTypes = includedContainerTypes; + } + + public List getIncludedContainerTypes() + { + return includedContainerTypes; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/search/RecordsManagementSearchService.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/search/RecordsManagementSearchService.java new file mode 100644 index 0000000000..b4e971bae5 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/search/RecordsManagementSearchService.java @@ -0,0 +1,92 @@ +/* + * 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.module.org_alfresco_module_rm.search; + +import java.util.List; + +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Records management search service. + * + * @author Roy Wetherall + */ +public interface RecordsManagementSearchService +{ + /** + * Execute a records management search + * @param siteId the id of the rm site to query + * @param query search query string + * @param searchParameters search parameters + * @return {@link List}<{@link NodeRef}> search results + */ + List search(String siteId, String query, RecordsManagementSearchParameters searchParameters); + + /** + * Get all the searches saved on the given records management site. + * @param siteId site id + * @return {@link List<{@link SavedSearchDetails}>} list of saved search details + */ + List getSavedSearches(String siteId); + + /** + * Get a named saved search for a given records management site. + * @param siteId site id + * @param name name of search + * @return {@link SavedSearchDetails} saved search details + */ + SavedSearchDetails getSavedSearch(String siteId, String name); + + /** + * Save records management search. + * @param siteId site id + * @param name name + * @param description description + * @param search search string + * @param isPublic indicates whether the saved search is public or not + * @return {@link SavedSearchDetails} details of the saved search + */ + SavedSearchDetails saveSearch(String siteId, String name, String description, String search, RecordsManagementSearchParameters searchParameters, boolean isPublic); + + /** + * Save records management search. + * @param savedSearchDetails details of search to save + * @return {@link SavedSearchDetails} details of the saved search + */ + SavedSearchDetails saveSearch(SavedSearchDetails savedSearchDetails); + + /** + * Delete saved search + * @param siteId site id + * @param name name of saved search + */ + void deleteSavedSearch(String siteId, String name); + + /** + * Delete saved search + * @param savedSearchDetails saved search details + */ + void deleteSavedSearch(SavedSearchDetails savedSearchDetails); + + /** + * Adds the reports as saved searches to a given site. + * @param siteId site id + */ + void addReports(String siteId); +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/search/RecordsManagementSearchServiceImpl.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/search/RecordsManagementSearchServiceImpl.java new file mode 100644 index 0000000000..73254dd6e4 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/search/RecordsManagementSearchServiceImpl.java @@ -0,0 +1,612 @@ +/* + * 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.module.org_alfresco_module_rm.search; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map.Entry; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.model.FileInfo; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.SearchParameters; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.cmr.site.SiteService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.ISO9075; +import org.alfresco.util.ParameterCheck; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Records management search service implementation + * + * @author Roy Wetherall + */ +public class RecordsManagementSearchServiceImpl implements RecordsManagementSearchService +{ + private static final String SITES_SPACE_QNAME_PATH = "/app:company_home/st:sites/"; + + /** Name of the main site container used to store the saved searches within */ + private static final String SEARCH_CONTAINER = "Saved Searches"; + + /** File folder service */ + private FileFolderService fileFolderService; + + /** Search service */ + private SearchService searchService; + + /** Site service */ + private SiteService siteService; + + /** Namespace service */ + private NamespaceService namespaceService; + + /** List of report details */ + private List reports = new ArrayList(13); + + /** + * @param fileFolderService file folder service + */ + public void setFileFolderService(FileFolderService fileFolderService) + { + this.fileFolderService = fileFolderService; + } + + /** + * @param searchService search service + */ + public void setSearchService(SearchService searchService) + { + this.searchService = searchService; + } + + /** + * @param siteService site service + */ + public void setSiteService(SiteService siteService) + { + this.siteService = siteService; + } + + /** + * @param namespaceService namespace service + */ + public void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + /** + * @param reportsJSON + */ + public void setReportsJSON(String reportsJSON) + { + try + { + JSONArray jsonArray = new JSONArray(reportsJSON); + if (jsonArray != null) + { + for (int i=0; i < jsonArray.length(); i++) + { + JSONObject report = jsonArray.getJSONObject(i); + + // Get the name + if (report.has(SavedSearchDetails.NAME) == false) + { + throw new AlfrescoRuntimeException("Unable to load report details because name has not been specified. \n" + reportsJSON); + } + String name = report.getString(SavedSearchDetails.NAME); + + // Get the query + if (report.has(SavedSearchDetails.SEARCH) == false) + { + throw new AlfrescoRuntimeException("Unable to load report details because search has not been specified for report " + name + ". \n" + reportsJSON); + } + String query = report.getString(SavedSearchDetails.SEARCH); + + // Get the description + String description = ""; + if (report.has(SavedSearchDetails.DESCRIPTION) == true) + { + description = report.getString(SavedSearchDetails.DESCRIPTION); + } + + RecordsManagementSearchParameters searchParameters = new RecordsManagementSearchParameters(); + if (report.has("searchparams") == true) + { + searchParameters = RecordsManagementSearchParameters.createFromJSON(report.getJSONObject("searchparams"), namespaceService); + } + + // Create the report details and add to list + ReportDetails reportDetails = new ReportDetails(name, description, query, searchParameters); + reports.add(reportDetails); + } + } + } + catch (JSONException exception) + { + throw new AlfrescoRuntimeException("Unable to load report details.\n" + reportsJSON, exception); + } + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.search.RecordsManagementSearchService#search(java.lang.String, java.lang.String, org.alfresco.module.org_alfresco_module_rm.search.RecordsManagementSearchParameters) + */ + @Override + public List search(String siteId, String query, RecordsManagementSearchParameters rmSearchParameters) + { + // build the full RM query + StringBuilder fullQuery = new StringBuilder(1024); + fullQuery.append("PATH:\"") + .append(SITES_SPACE_QNAME_PATH) + .append("cm:").append(ISO9075.encode(siteId)).append("/cm:documentLibrary//*\"") + .append(" AND (") + .append(buildQueryString(query, rmSearchParameters)) + .append(")"); + + // create the search parameters + SearchParameters searchParameters = new SearchParameters(); + searchParameters.setQuery(fullQuery.toString()); + searchParameters.setLanguage(SearchService.LANGUAGE_FTS_ALFRESCO); + searchParameters.addStore(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE); + searchParameters.setMaxItems(rmSearchParameters.getMaxItems()); + searchParameters.setNamespace(RecordsManagementModel.RM_URI); + + // set sort + for(Entry entry : rmSearchParameters.getSortOrder().entrySet()) + { + searchParameters.addSort(entry.getKey().toPrefixString(namespaceService), entry.getValue().booleanValue()); + } + + // set templates + for (Entry entry : rmSearchParameters.getTemplates().entrySet()) + { + searchParameters.addQueryTemplate(entry.getKey(), entry.getValue()); + } + + // execute query + ResultSet resultSet = searchService.query(searchParameters); + + // return results + return resultSet.getNodeRefs(); + } + + /** + * + * @param queryTerm + * @param aspects + * @param types + * @return + */ + /*package*/ String buildQueryString(String queryTerm, RecordsManagementSearchParameters searchParameters) + { + StringBuilder aspectQuery = new StringBuilder(); + if (searchParameters.isIncludeRecords() == true) + { + appendAspect(aspectQuery, "rma:record"); + if (searchParameters.isIncludeUndeclaredRecords() == false) + { + appendAspect(aspectQuery, "rma:declaredRecord"); + } + if (searchParameters.isIncludeVitalRecords() == true) + { + appendAspect(aspectQuery, "rma:vitalRecord"); + } + } + + StringBuilder typeQuery = new StringBuilder(); + if (searchParameters.isIncludeRecordFolders() == true) + { + appendType(typeQuery, "rma:recordFolder"); + } + List includedContainerTypes = searchParameters.getIncludedContainerTypes(); + if (includedContainerTypes != null && includedContainerTypes.size() != 0) + { + for (QName includedContainerType : includedContainerTypes) + { + appendType(typeQuery, includedContainerType.toPrefixString(namespaceService)); + } + } + + StringBuilder query = new StringBuilder(); + if (queryTerm == null || queryTerm.length() == 0) + { + // Default to search for everything + query.append("ISNODE:T"); + } + else + { + if (isComplexQueryTerm(queryTerm) == true) + { + query.append(queryTerm); + } + else + { + query.append("keywords:\"" + queryTerm + "\""); + } + } + + StringBuilder fullQuery = new StringBuilder(1024); + if (aspectQuery.length() != 0 || typeQuery.length() != 0) + { + if (aspectQuery.length() != 0 && typeQuery.length() != 0) + { + fullQuery.append("("); + } + + if (aspectQuery.length() != 0) + { + fullQuery.append("(").append(aspectQuery).append(") "); + } + + if (typeQuery.length() != 0) + { + fullQuery.append("(").append(typeQuery).append(")"); + } + + if (aspectQuery.length() != 0 && typeQuery.length() != 0) + { + fullQuery.append(")"); + } + } + + if (searchParameters.isIncludeFrozen() == true) + { + appendAspect(fullQuery, "rma:frozen"); + } + else + { + appendNotAspect(fullQuery, "rma:frozen"); + } + if (searchParameters.isIncludeCutoff() == true) + { + appendAspect(fullQuery, "rma:cutOff"); + } + + if (fullQuery.length() != 0) + { + fullQuery.append(" AND "); + } + fullQuery.append("(") + .append(query) + .append(") AND NOT ASPECT:\"rma:versionedRecord\""); + + return fullQuery.toString(); + } + + private boolean isComplexQueryTerm(String query) + { + return query.matches(".*[\":].*"); + } + + /** + * + * @param sb + * @param aspect + */ + private void appendAspect(StringBuilder sb, String aspect) + { + appendWithJoin(sb, " AND ", "ASPECT:\"", aspect, "\""); + } + + private void appendNotAspect(StringBuilder sb, String aspect) + { + appendWithJoin(sb, " AND ", "NOT ASPECT:\"", aspect, "\""); + } + + /** + * + * @param sb + * @param type + */ + private void appendType(StringBuilder sb, String type) + { + appendWithJoin(sb, " ", "TYPE:\"", type, "\""); + } + + /** + * + * @param sb + * @param withJoin + * @param prefix + * @param value + * @param postfix + */ + private void appendWithJoin(StringBuilder sb, String withJoin, String prefix, String value, String postfix) + { + if (sb.length() != 0) + { + sb.append(withJoin); + } + sb.append(prefix).append(value).append(postfix); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.search.RecordsManagementSearchService#getSavedSearches(java.lang.String) + */ + @Override + public List getSavedSearches(String siteId) + { + List result = new ArrayList(17); + + NodeRef container = siteService.getContainer(siteId, SEARCH_CONTAINER); + if (container != null) + { + // add the details of all the public saved searches + List searches = fileFolderService.listFiles(container); + for (FileInfo search : searches) + { + addSearchDetailsToList(result, search.getNodeRef()); + } + + // add the details of any "private" searches for the current user + String userName = AuthenticationUtil.getFullyAuthenticatedUser(); + NodeRef userContainer = fileFolderService.searchSimple(container, userName); + if (userContainer != null) + { + List userSearches = fileFolderService.listFiles(userContainer); + for (FileInfo userSearch : userSearches) + { + addSearchDetailsToList(result, userSearch.getNodeRef()); + } + } + } + + return result; + } + + /** + * Add the search details to the list. + * @param searches list of search details + * @param searchNode search node + */ + private void addSearchDetailsToList(List searches, NodeRef searchNode) + { + ContentReader reader = fileFolderService.getReader(searchNode); + String jsonString = reader.getContentString(); + SavedSearchDetails savedSearchDetails = SavedSearchDetails.createFromJSON(jsonString, namespaceService, this); + searches.add(savedSearchDetails); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.search.RecordsManagementSearchService#getSavedSearch(java.lang.String, java.lang.String) + */ + @Override + public SavedSearchDetails getSavedSearch(String siteId, String name) + { + // check for mandatory parameters + ParameterCheck.mandatory("siteId", siteId); + ParameterCheck.mandatory("name", name); + + SavedSearchDetails result = null; + + // get the saved search node + NodeRef searchNode = getSearchNodeRef(siteId, name); + + if (searchNode != null) + { + // get the json content + ContentReader reader = fileFolderService.getReader(searchNode); + String jsonString = reader.getContentString(); + + // create the saved search details + result = SavedSearchDetails.createFromJSON(jsonString, namespaceService, this); + } + + return result; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.search.RecordsManagementSearchService#saveSearch(java.lang.String, java.lang.String, java.lang.String, java.lang.String, boolean) + */ + @Override + public SavedSearchDetails saveSearch(String siteId, String name, String description, String query, RecordsManagementSearchParameters searchParameters, boolean isPublic) + { + // Check for mandatory parameters + ParameterCheck.mandatory("siteId", siteId); + ParameterCheck.mandatory("name", name); + ParameterCheck.mandatory("query", query); + ParameterCheck.mandatory("searchParameters", searchParameters); + + // Create saved search details + SavedSearchDetails savedSearchDetails = new SavedSearchDetails(siteId, name, description, query, searchParameters, isPublic, false, namespaceService, this); + + // Save search details + return saveSearch(savedSearchDetails); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.search.RecordsManagementSearchService#saveSearch(org.alfresco.module.org_alfresco_module_rm.search.SavedSearchDetails) + */ + @Override + public SavedSearchDetails saveSearch(final SavedSearchDetails savedSearchDetails) + { + // Check for mandatory parameters + ParameterCheck.mandatory("savedSearchDetails", savedSearchDetails); + + // Get the root saved search container + final String siteId = savedSearchDetails.getSiteId(); + NodeRef container = siteService.getContainer(siteId, SEARCH_CONTAINER); + if (container == null) + { + container = AuthenticationUtil.runAs(new RunAsWork() + { + @Override + public NodeRef doWork() throws Exception + { + return siteService.createContainer(siteId, SEARCH_CONTAINER, null, null); + } + }, AuthenticationUtil.getSystemUserName()); + } + + // Get the private container for the current user + if (savedSearchDetails.isPublic() == false) + { + final String userName = AuthenticationUtil.getFullyAuthenticatedUser(); + NodeRef userContainer = fileFolderService.searchSimple(container, userName); + if (userContainer == null) + { + final NodeRef parentContainer = container; + userContainer = AuthenticationUtil.runAs(new RunAsWork() + { + @Override + public NodeRef doWork() throws Exception + { + return fileFolderService.create(parentContainer, userName, ContentModel.TYPE_FOLDER).getNodeRef(); + } + }, AuthenticationUtil.getSystemUserName()); + } + container = userContainer; + } + + // Get the saved search node + NodeRef searchNode = fileFolderService.searchSimple(container, savedSearchDetails.getName()); + if (searchNode == null) + { + final NodeRef searchContainer = container; + searchNode = AuthenticationUtil.runAs(new RunAsWork() + { + @Override + public NodeRef doWork() throws Exception + { + return fileFolderService.create(searchContainer, savedSearchDetails.getName(), ContentModel.TYPE_CONTENT).getNodeRef(); + } + }, AuthenticationUtil.getSystemUserName()); + } + + // Write the JSON content to search node + final NodeRef writableSearchNode = searchNode; + AuthenticationUtil.runAs(new RunAsWork() + { + @Override + public Void doWork() throws Exception + { + ContentWriter writer = fileFolderService.getWriter(writableSearchNode); + writer.setEncoding("UTF-8"); + writer.setMimetype(MimetypeMap.MIMETYPE_JSON); + writer.putContent(savedSearchDetails.toJSONString()); + + return null; + } + }, AuthenticationUtil.getSystemUserName()); + + return savedSearchDetails; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.search.RecordsManagementSearchService#deleteSavedSearch(java.lang.String, java.lang.String) + */ + @Override + public void deleteSavedSearch(String siteId, String name) + { + // Check parameters + ParameterCheck.mandatory("siteId", siteId); + ParameterCheck.mandatory("name", name); + + // Get the search node for the saved query + NodeRef searchNode = getSearchNodeRef(siteId, name); + if (searchNode != null && fileFolderService.exists(searchNode) == true) + { + fileFolderService.delete(searchNode); + } + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.search.RecordsManagementSearchService#deleteSavedSearch(org.alfresco.module.org_alfresco_module_rm.search.SavedSearchDetails) + */ + @Override + public void deleteSavedSearch(SavedSearchDetails savedSearchDetails) + { + // Check parameters + ParameterCheck.mandatory("savedSearchDetails", savedSearchDetails); + + // Delete the saved search + deleteSavedSearch(savedSearchDetails.getSiteId(), savedSearchDetails.getName()); + } + + /** + * Get the saved search node reference. + * @param siteId site id + * @param name search name + * @return {@link NodeRef} search node reference + */ + private NodeRef getSearchNodeRef(String siteId, String name) + { + NodeRef searchNode = null; + + // Get the root saved search container + NodeRef container = siteService.getContainer(siteId, SEARCH_CONTAINER); + if (container != null) + { + // try and find the search node + searchNode = fileFolderService.searchSimple(container, name); + + // can't find it so check the users container + if (searchNode == null) + { + String userName = AuthenticationUtil.getFullyAuthenticatedUser(); + NodeRef userContainer = fileFolderService.searchSimple(container, userName); + if (userContainer != null) + { + searchNode = fileFolderService.searchSimple(userContainer, name); + } + } + } + + return searchNode; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.search.RecordsManagementSearchService#addReports(java.lang.String) + */ + @Override + public void addReports(String siteId) + { + for (ReportDetails report : reports) + { + // Create saved search details + SavedSearchDetails savedSearchDetails = new SavedSearchDetails( + siteId, + report.getName(), + report.getDescription(), + report.getSearch(), + report.getSearchParameters(), + true, + true, + namespaceService, + this); + + // Save search details + saveSearch(savedSearchDetails); + } + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/search/ReportDetails.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/search/ReportDetails.java new file mode 100644 index 0000000000..4275a01907 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/search/ReportDetails.java @@ -0,0 +1,111 @@ +/* + * 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.module.org_alfresco_module_rm.search; + + +/** + * Report details. + * + * @author Roy Wetherall + */ +public class ReportDetails +{ + /** Name */ + protected String name; + + /** Description */ + protected String description; + + /** Search */ + protected String search; + + /** Search parameters */ + protected RecordsManagementSearchParameters searchParameters; + + /** + * + * @param name + * @param description + * @param search + * @param searchParameters + */ + public ReportDetails(String name, String description, String search, RecordsManagementSearchParameters searchParameters) + { + this.name = name; + this.description = description; + this.search = search; + this.searchParameters = searchParameters; + } + + /** + * @return {@link String} name + */ + public String getName() + { + return name; + } + + /** + * @return {@link String} description + */ + public String getDescription() + { + return description; + } + + /** + * @param description description + */ + public void setDescription(String description) + { + this.description = description; + } + + /** + * @return {@link String} search string + */ + public String getSearch() + { + return search; + } + + /** + * @param query query string + */ + public void setSearch(String search) + { + this.search = search; + } + + /** + * @return + */ + public RecordsManagementSearchParameters getSearchParameters() + { + return searchParameters; + } + + /** + * @param searchParameters + */ + public void setSearchParameters(RecordsManagementSearchParameters searchParameters) + { + this.searchParameters = searchParameters; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/search/SavedSearchDetails.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/search/SavedSearchDetails.java new file mode 100644 index 0000000000..4c0dc935a1 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/search/SavedSearchDetails.java @@ -0,0 +1,287 @@ +/* + * 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.module.org_alfresco_module_rm.search; + + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.util.ParameterCheck; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Saved search details. + * + * Example format of posted Saved Search JSON: + * + * { + * "siteid" : "rm", + * "name": "search name", + * "description": "the search description", + * "search": "the search sting as entered by the user", + * "public": boolean, + * "searchparams" : + * { + * "maxItems" : 500, + * "records" : true, + * "undeclaredrecords" : false, + * "vitalrecords" : false, + * "recordfolders" : false, + * "frozen" : false, + * "cutoff" : false, + * "containertypes" : + * [ + * "rma:recordSeries", + * "rma:recordCategory" + * ] + * "sort" : + * [ + * { + * "field" : "cm:name", + * "ascending" : true + * } + * ] + * } + * } + * + * where: name and query values are mandatory, + * searchparams contains the filters, sort, etc information about the query + * query is there for backward compatibility + * note: + * "params": "terms=keywords:xyz&undeclared=true", + * "sort": "cm:name/asc" + * "query": "the complete search query string", + * ... are sometimes found in the place of searchparams and are migrated to the new format when re-saved + * params are in URL encoded name/value pair format + * sort is in comma separated "property/dir" packed format i.e. "cm:name/asc,cm:title/desc" + * + * @author Roy Wetherall + */ +public class SavedSearchDetails extends ReportDetails +{ + // JSON label values + public static final String SITE_ID = "siteid"; + public static final String NAME = "name"; + public static final String DESCRIPTION = "description"; + public static final String SEARCH = "search"; + public static final String PUBLIC = "public"; + public static final String REPORT = "report"; + public static final String SEARCHPARAMS = "searchparams"; + + // JSON values for backwards compatibility + public static final String QUERY = "query"; + public static final String SORT = "sort"; + public static final String PARAMS = "params"; + + /** Site id */ + private String siteId; + + /** Indicates whether the saved search is public or not */ + private boolean isPublic; + + /** Indicates whether the saved search is a report */ + private boolean isReport = false; + + /** Namespace service */ + NamespaceService namespaceService; + + RecordsManagementSearchServiceImpl searchService; + + private SavedSearchDetailsCompatibility compatibility; + + /** + * + * @param jsonString + * @return + */ + /*package*/ static SavedSearchDetails createFromJSON(String jsonString, NamespaceService namespaceService, RecordsManagementSearchServiceImpl searchService) + { + try + { + JSONObject search = new JSONObject(jsonString); + + // Get the site id + if (search.has(SITE_ID) == false) + { + throw new AlfrescoRuntimeException("Can not create saved search details from json, because required siteid is not present. " + jsonString); + } + String siteId = search.getString(SITE_ID); + + // Get the name + if (search.has(NAME) == false) + { + throw new AlfrescoRuntimeException("Can not create saved search details from json, because required name is not present. " + jsonString); + } + String name = search.getString(NAME); + + // Get the description + String description = ""; + if (search.has(DESCRIPTION) == true) + { + description = search.getString(DESCRIPTION); + } + + // Get the query + String query = null; + if (search.has(SEARCH) == false) + { + // We are probably dealing with a "old" style saved search + if (search.has(PARAMS) == true) + { + String oldParams = search.getString(PARAMS); + query = SavedSearchDetailsCompatibility.getSearchFromParams(oldParams); + } + else + { + throw new AlfrescoRuntimeException("Can not create saved search details from json, because required search is not present. " + jsonString); + } + + } + else + { + query = search.getString(SEARCH); + } + + // Get the search parameters + RecordsManagementSearchParameters searchParameters = new RecordsManagementSearchParameters(); + if (search.has(SEARCHPARAMS) == true) + { + searchParameters = RecordsManagementSearchParameters.createFromJSON(search.getJSONObject(SEARCHPARAMS), namespaceService); + } + else + { + // See if we are dealing with the old style of saved search + if (search.has(PARAMS) == true) + { + String oldParams = search.getString(PARAMS); + String oldSort = search.getString(SORT); + searchParameters = SavedSearchDetailsCompatibility.createSearchParameters(oldParams, oldSort, namespaceService); + } + } + + // Determine whether the saved query is public or not + boolean isPublic = false; + if (search.has(PUBLIC) == true) + { + isPublic = search.getBoolean(PUBLIC); + } + + // Determine whether the saved query is a report or not + boolean isReport = false; + if (search.has(REPORT) == true) + { + isReport = search.getBoolean(REPORT); + } + + // Create the saved search details object + return new SavedSearchDetails(siteId, name, description, query, searchParameters, isPublic, isReport, namespaceService, searchService); + } + catch (JSONException exception) + { + throw new AlfrescoRuntimeException("Can not create saved search details from json. " + jsonString, exception); + } + } + + /** + * @param siteId + * @param name + * @param description + * @param isPublic + */ + /*package*/ SavedSearchDetails( + String siteId, + String name, + String description, + String serach, + RecordsManagementSearchParameters searchParameters, + boolean isPublic, + boolean isReport, + NamespaceService namespaceService, + RecordsManagementSearchServiceImpl searchService) + { + super(name, description, serach, searchParameters); + + ParameterCheck.mandatory("siteId", siteId); + ParameterCheck.mandatory("namespaceService", namespaceService); + ParameterCheck.mandatory("searchService", searchService); + + this.siteId = siteId; + this.isPublic = isPublic; + this.isReport = isReport; + this.namespaceService = namespaceService; + this.compatibility = new SavedSearchDetailsCompatibility(this, namespaceService, searchService); + this.searchService = searchService; + } + + /** + * @return + */ + public String getSiteId() + { + return siteId; + } + + /** + * @return + */ + public boolean isPublic() + { + return isPublic; + } + + /** + * @return + */ + public boolean isReport() + { + return isReport; + } + + public SavedSearchDetailsCompatibility getCompatibility() + { + return compatibility; + } + + /** + * @return + */ + /*package*/ String toJSONString() + { + try + { + JSONObject jsonObject = new JSONObject(); + jsonObject.put(SITE_ID, siteId); + jsonObject.put(NAME, name); + jsonObject.put(DESCRIPTION, description); + jsonObject.put(SEARCH, search); + jsonObject.put(SEARCHPARAMS, searchParameters.toJSONObject(namespaceService)); + jsonObject.put(PUBLIC, isPublic); + + // Add full query for backward compatibility + jsonObject.put(QUERY, searchService.buildQueryString(search, searchParameters)); + jsonObject.put(SORT, compatibility.getSort()); + + return jsonObject.toString(); + } + catch (JSONException exception) + { + throw new AlfrescoRuntimeException("Can not convert saved search details into JSON.", exception); + } + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/search/SavedSearchDetailsCompatibility.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/search/SavedSearchDetailsCompatibility.java new file mode 100644 index 0000000000..c4882ec968 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/search/SavedSearchDetailsCompatibility.java @@ -0,0 +1,229 @@ +/* + * 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.module.org_alfresco_module_rm.search; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; + +/** + * Compatibility class. + * + * Used to bridge between the old style of saved search passed and required by the UI and the new actual saved search details. + * Eventually will be factored out as web scripts are brought up to date. + */ +public class SavedSearchDetailsCompatibility implements RecordsManagementModel +{ + /** Saved search details */ + private final SavedSearchDetails savedSearchDetails; + + /** Namespace service */ + private final NamespaceService namespaceService; + + /** Records management search service implementation */ + private final RecordsManagementSearchServiceImpl searchService; + + /** + * Retrieve the search from the parameter string + * @param params parameter string + * @return String search term + */ + public static String getSearchFromParams(String params) + { + String search = null; + String[] values = params.split("&"); + for (String value : values) + { + if (value.startsWith("terms") == true) + { + String[] terms = value.trim().split("="); + try + { + search = URLDecoder.decode(terms[1], "UTF-8"); + } + catch (UnsupportedEncodingException e) + { + // Do nothing just return null + search = null; + } + break; + } + } + + return search; + } + + public static RecordsManagementSearchParameters createSearchParameters(String params, String sort, NamespaceService namespaceService) + { + return createSearchParameters(params, new String[]{"&", "="}, sort, namespaceService); + } + + /** + * + * @param params + * @param sort + * @param namespaceService + * @return + */ + public static RecordsManagementSearchParameters createSearchParameters(String params, String[] paramsDelim, String sort, NamespaceService namespaceService) + { + RecordsManagementSearchParameters result = new RecordsManagementSearchParameters(); + List includedContainerTypes = new ArrayList(2); + + // Map the param values into the search parameter object + String[] values = params.split(paramsDelim[0]); + for (String value : values) + { + String[] paramValues = value.split(paramsDelim[1]); + String paramName = paramValues[0].trim(); + String paramValue = paramValues[1].trim(); + if ("records".equals(paramName) == true) + { + result.setIncludeRecords(Boolean.parseBoolean(paramValue)); + } + else if ("undeclared".equals(paramName) == true) + { + result.setIncludeUndeclaredRecords(Boolean.parseBoolean(paramValue)); + } + else if ("vital".equals(paramName) == true) + { + result.setIncludeVitalRecords(Boolean.parseBoolean(paramValue)); + } + else if ("folders".equals(paramName) == true) + { + result.setIncludeRecordFolders(Boolean.parseBoolean(paramValue)); + } + else if ("frozen".equals(paramName) == true) + { + result.setIncludeFrozen(Boolean.parseBoolean(paramValue)); + } + else if ("cutoff".equals(paramName) == true) + { + result.setIncludeCutoff(Boolean.parseBoolean(paramValue)); + } + else if ("categories".equals(paramName) == true && Boolean.parseBoolean(paramValue) == true) + { + includedContainerTypes.add(TYPE_RECORD_CATEGORY); + } +// else if ("series".equals(paramName) == true && Boolean.parseBoolean(paramValue) == true) +// { +// includedContainerTypes.add(DOD5015Model.TYPE_RECORD_SERIES); +// } + } + result.setIncludedContainerTypes(includedContainerTypes); + + if (sort != null) + { + // Map the sort string into the search details + String[] sortPairs = sort.split(","); + Map sortOrder = new HashMap(sortPairs.length); + for (String sortPairString : sortPairs) + { + String[] sortPair = sortPairString.split("/"); + QName field = QName.createQName(sortPair[0], namespaceService); + Boolean isAcsending = Boolean.FALSE; + if ("asc".equals(sortPair[1]) == true) + { + isAcsending = Boolean.TRUE; + } + sortOrder.put(field, isAcsending); + } + result.setSortOrder(sortOrder); + } + + return result; + } + + /** + * Constructor + * @param savedSearchDetails + */ + public SavedSearchDetailsCompatibility(SavedSearchDetails savedSearchDetails, + NamespaceService namespaceService, + RecordsManagementSearchServiceImpl searchService) + { + this.savedSearchDetails = savedSearchDetails; + this.namespaceService = namespaceService; + this.searchService = searchService; + } + + /** + * Get the sort string from the saved search details + * @return + */ + public String getSort() + { + StringBuilder builder = new StringBuilder(64); + + for (Map.Entry entry : this.savedSearchDetails.getSearchParameters().getSortOrder().entrySet()) + { + if (builder.length() !=0) + { + builder.append(","); + } + + String order = "desc"; + if (Boolean.TRUE.equals(entry.getValue()) == true) + { + order = "asc"; + } + builder.append(entry.getKey().toPrefixString(this.namespaceService)) + .append("/") + .append(order); + } + + return builder.toString(); + } + + /** + * Get the parameter string from the saved search details + * @return + */ + public String getParams() + { + List includeContainerTypes = this.savedSearchDetails.getSearchParameters().getIncludedContainerTypes(); + StringBuilder builder = new StringBuilder(128); + builder.append("terms=").append(this.savedSearchDetails.getSearch()).append("&") + .append("records=").append(this.savedSearchDetails.getSearchParameters().isIncludeRecords()).append("&") + .append("undeclared=").append(this.savedSearchDetails.getSearchParameters().isIncludeUndeclaredRecords()).append("&") + .append("vital=").append(this.savedSearchDetails.getSearchParameters().isIncludeVitalRecords()).append("&") + .append("folders=").append(this.savedSearchDetails.getSearchParameters().isIncludeRecordFolders()).append("&") + .append("frozen=").append(this.savedSearchDetails.getSearchParameters().isIncludeFrozen()).append("&") + .append("cutoff=").append(this.savedSearchDetails.getSearchParameters().isIncludeCutoff()).append("&") + .append("categories=").append(includeContainerTypes.contains(TYPE_RECORD_CATEGORY)).append("&") + .append("series=").append(false); + return builder.toString(); + } + + /** + * Build the full query string + * @return + */ + public String getQuery() + { + return searchService.buildQueryString(this.savedSearchDetails.getSearch(), this.savedSearchDetails.getSearchParameters()); + } +} \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/security/RecordsManagementSecurityService.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/security/RecordsManagementSecurityService.java new file mode 100644 index 0000000000..781844762b --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/security/RecordsManagementSecurityService.java @@ -0,0 +1,186 @@ +/* + * 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.module.org_alfresco_module_rm.security; + +import java.util.Map; +import java.util.Set; + +import org.alfresco.module.org_alfresco_module_rm.capability.Capability; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.namespace.QName; + +/** + * Records management permission service interface + * + * @author Roy Wetherall + */ +public interface RecordsManagementSecurityService +{ + /** + * Get a list of the capabilities available + * + * @return List list of capabilities available + */ + Set getCapabilities(); + + /** + * Get the full set of capabilities for the current user. + * @param nodeRef + * @return + */ + Map getCapabilities(NodeRef nodeRef); + + /** + * + * @param nodeRef + * @param capabilitySet + * @return + */ + Map getCapabilities(NodeRef nodeRef, String capabilitySet); + + /** + * Get a capability by name + * @param name + * @return + */ + Capability getCapability(String name); + + /** + * Get the set of aspect QNames which can not be added direct via the public node service; + * they must be managed via the appropriate actions. + * @return + */ + Set getProtectedAspects(); + + /** + * Get the set of property QNames which can not be added, updated or removed direct via the public node service; + * they must be managed via the appropriate actions. + * @return + */ + Set getProtectedProperties(); + + /** + * Creates the initial set of default roles for a root records management node + * + * @param rmRootNode + */ + void bootstrapDefaultRoles(NodeRef rmRootNode); + + /** + * Get all the available roles for the given records management root node + * + * @param rmRootNode + * @return + */ + Set getRoles(NodeRef rmRootNode); + + /** + * Gets the roles for a given user + * + * @param rmRootNode + * @param user + * @return + */ + Set getRolesByUser(NodeRef rmRootNode, String user); + + /** + * Get a role by name + * + * @param rmRootNode + * @param role + * @return + */ + Role getRole(NodeRef rmRootNode, String role); + + /** + * Indicate whether a role exists for a given records management root node + * @param rmRootNode + * @param role + * @return + */ + boolean existsRole(NodeRef rmRootNode, String role); + + /** + * Determines whether the given user has the RM Admin role + * + * @param rmRootNode RM root node + * @param user user name to check + * @return true if the user has the RM Admin role, false otherwise + */ + boolean hasRMAdminRole(NodeRef rmRootNode, String user); + + /** + * Create a new role + * + * @param rmRootNode + * @param role + * @param roleDisplayLabel + * @param capabilities + * @return + */ + Role createRole(NodeRef rmRootNode, String role, String roleDisplayLabel, Set capabilities); + + /** + * Update an existing role + * + * @param rmRootNode + * @param role + * @param roleDisplayLabel + * @param capabilities + * @return + */ + Role updateRole(NodeRef rmRootNode, String role, String roleDisplayLabel, Set capabilities); + + /** + * Delete a role + * + * @param rmRootNode + * @param role + */ + void deleteRole(NodeRef rmRootNode, String role); + + /** + * Assign a role to an authority + * + * @param authorityName + * @param rmRootNode + * @param role + */ + void assignRoleToAuthority(NodeRef rmRootNode, String role, String authorityName); + + /** + * Sets a permission on a RM object. Assumes allow is true. Cascades permission down to record folder. + * Cascades ReadRecord up to file plan. + * + * @param nodeRef node reference + * @param authority authority + * @param permission permission + */ + void setPermission(NodeRef nodeRef, String authority, String permission); + + /** + * Deletes a permission from a RM object. Cascades removal down to record folder. + * + * @param nodeRef node reference + * @param authority authority + * @param permission permission + */ + void deletePermission(NodeRef nodeRef, String authority, String permission); +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/security/RecordsManagementSecurityServiceImpl.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/security/RecordsManagementSecurityServiceImpl.java new file mode 100644 index 0000000000..b9284eecd8 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/security/RecordsManagementSecurityServiceImpl.java @@ -0,0 +1,975 @@ +/* + * 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.module.org_alfresco_module_rm.security; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.capability.Capability; +import org.alfresco.module.org_alfresco_module_rm.capability.CapabilityService; +import org.alfresco.module.org_alfresco_module_rm.capability.RMEntryVoter; +import org.alfresco.module.org_alfresco_module_rm.capability.RMPermissionModel; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.repo.node.NodeServicePolicies; +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.policy.Behaviour.NotificationFrequency; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.permissions.AccessDeniedException; +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.cmr.security.AccessPermission; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.AuthorityService; +import org.alfresco.service.cmr.security.AuthorityType; +import org.alfresco.service.cmr.security.OwnableService; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.alfresco.util.ParameterCheck; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Records management permission service implementation + * + * @author Roy Wetherall + */ +public class RecordsManagementSecurityServiceImpl implements RecordsManagementSecurityService, + RecordsManagementModel + +{ + /** Capability service */ + private CapabilityService capabilityService; + + /** Authority service */ + private AuthorityService authorityService; + + /** Permission service */ + private PermissionService permissionService; + + /** Policy component */ + private PolicyComponent policyComponent; + + /** Owner service */ + private OwnableService ownableService; + + /** Records management service */ + private RecordsManagementService recordsManagementService; + + /** Node service */ + private NodeService nodeService; + + /** RM Entry voter */ + private RMEntryVoter voter; + + /** + * Capability sets. Allow sub-sets of capabilities to be defined enhancing performance when + * only a sub-set need be evaluated. + */ + private Map> capabilitySets; + + /** Records management role zone */ + public static final String RM_ROLE_ZONE_PREFIX = "rmRoleZone"; + + /** Logger */ + private static Log logger = LogFactory.getLog(RecordsManagementSecurityServiceImpl.class); + + /** + * Set the capability service + * + * @param capabilityService + */ + public void setCapabilityService(CapabilityService capabilityService) + { + this.capabilityService = capabilityService; + } + + /** + * Set the authortiy service + * + * @param authorityService + */ + public void setAuthorityService(AuthorityService authorityService) + { + this.authorityService = authorityService; + } + + /** + * Set the permission service + * + * @param permissionService + */ + public void setPermissionService(PermissionService permissionService) + { + this.permissionService = permissionService; + } + + /** + * Set the policy component + * + * @param policyComponent + */ + public void setPolicyComponent(PolicyComponent policyComponent) + { + this.policyComponent = policyComponent; + } + + /** + * Set the ownable service + * + * @param ownableService ownable service + */ + public void setOwnableService(OwnableService ownableService) + { + this.ownableService = ownableService; + } + + /** + * Set records management service + * + * @param recordsManagementService records management service + */ + public void setRecordsManagementService(RecordsManagementService recordsManagementService) + { + this.recordsManagementService = recordsManagementService; + } + + /** + * Set the node service + * + * @param nodeService + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Set the capability sets + * @param capabilitySets map of capability sets (configured in Spring) + */ + public void setCapabilitySets(Map> capabilitySets) + { + this.capabilitySets = capabilitySets; + } + + /** + * Set the RM voter + * + * @param voter + */ + public void setVoter(RMEntryVoter voter) + { + this.voter = voter; + } + + /** + * Initialisation method + */ + public void init() + { + policyComponent.bindClassBehaviour(NodeServicePolicies.OnCreateNodePolicy.QNAME, + TYPE_FILE_PLAN, + new JavaBehaviour(this, "onCreateRootNode", NotificationFrequency.TRANSACTION_COMMIT)); + policyComponent.bindClassBehaviour(NodeServicePolicies.OnCreateNodePolicy.QNAME, + TYPE_RECORD_CATEGORY, + new JavaBehaviour(this, "onCreateRMContainer", NotificationFrequency.TRANSACTION_COMMIT)); + policyComponent.bindClassBehaviour(NodeServicePolicies.OnCreateNodePolicy.QNAME, + TYPE_RECORD_FOLDER, + new JavaBehaviour(this, "onCreateRecordFolder", NotificationFrequency.TRANSACTION_COMMIT)); + policyComponent.bindClassBehaviour(NodeServicePolicies.BeforeDeleteNodePolicy.QNAME, + ASPECT_FROZEN, + new JavaBehaviour(this, "beforeDeleteFrozenNode", NotificationFrequency.TRANSACTION_COMMIT)); + } + + public void beforeDeleteFrozenNode(NodeRef nodeRef) + { + throw new AccessDeniedException("Frozen nodes can not be deleted"); + } + + /** + * Create root node behaviour + * + * @param childAssocRef + */ + public void onCreateRootNode(ChildAssociationRef childAssocRef) + { + final NodeRef rmRootNode = childAssocRef.getChildRef(); + + // Do not execute behaviour if this has been created in the archive store + if(rmRootNode.getStoreRef().equals(StoreRef.STORE_REF_ARCHIVE_SPACESSTORE) == true) + { + // This is not the spaces store - probably the archive store + return; + } + + if (nodeService.exists(rmRootNode) == true) + { + AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public Object doWork() + { + // Create "all" role group for root node + String allRoles = authorityService.createAuthority(AuthorityType.GROUP, getAllRolesGroupShortName(rmRootNode), "All Roles", null); + + // Set the permissions + permissionService.setInheritParentPermissions(rmRootNode, false); + permissionService.setPermission(rmRootNode, allRoles, RMPermissionModel.READ_RECORDS, true); + return null; + } + }, AuthenticationUtil.getAdminUserName()); + + // Bootstrap in the default set of roles for the newly created root node + bootstrapDefaultRoles(rmRootNode); + } + } + + /** + * Delete root node behaviour + * + * @param childAssocRef + */ + public void onDeleteRootNode(NodeRef rmRootNode) + { + logger.debug("onDeleteRootNode called"); + } + + /** + * Get all the roles by short name + * + * @param rmRootNode + * @return + */ + private String getAllRolesGroupShortName(NodeRef rmRootNode) + { + return "AllRoles" + rmRootNode.getId(); + } + + /** + * @param childAssocRef + */ + public void onCreateRMContainer(ChildAssociationRef childAssocRef) + { + setUpPermissions(childAssocRef.getChildRef()); + } + + /** + * @param childAssocRef + */ + public void onCreateRecordFolder(ChildAssociationRef childAssocRef) + { + final NodeRef folderNodeRef = childAssocRef.getChildRef(); + setUpPermissions(folderNodeRef); + + // Pull any permissions found on the parent (ie the record category) + final NodeRef catNodeRef = childAssocRef.getParentRef(); + if (nodeService.exists(catNodeRef) == true) + { + AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public Object doWork() + { + Set perms = permissionService.getAllSetPermissions(catNodeRef); + for (AccessPermission perm : perms) + { + AccessStatus accessStatus = perm.getAccessStatus(); + boolean allow = false; + if (AccessStatus.ALLOWED.equals(accessStatus) == true) + { + allow = true; + } + permissionService.setPermission( + folderNodeRef, + perm.getAuthority(), + perm.getPermission(), + allow); + } + + return null; + } + }, AuthenticationUtil.getAdminUserName()); + } + } + + /** + * + * @param nodeRef + */ + public void setUpPermissions(final NodeRef nodeRef) + { + if (nodeService.exists(nodeRef) == true) + { + AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public Object doWork() + { + // Break inheritance + permissionService.setInheritParentPermissions(nodeRef, false); + + return null; + } + }, AuthenticationUtil.getAdminUserName()); + } + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService#getCapabilities() + */ + public Set getCapabilities() + { + Collection caps = capabilityService.getCapabilities(); + Set result = new HashSet(caps.size()); + for (Capability cap : caps) + { + if (cap.isGroupCapability() == false) + { + result.add(cap); + } + } + return result; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService#getCapabilities(org.alfresco.service.cmr.repository.NodeRef) + */ + public Map getCapabilities(NodeRef nodeRef) + { + return capabilityService.getCapabilitiesAccessState(nodeRef); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService#getCapabilities(org.alfresco.service.cmr.repository.NodeRef, java.lang.String) + */ + public Map getCapabilities(NodeRef nodeRef, String capabilitySet) + { + List capabilities = capabilitySets.get(capabilitySet); + if (capabilities == null) + { + if (getCapability(capabilitySet) != null) + { + // If the capability set is the name of a capability assume we just want that single + // capability + capabilities = new ArrayList(1); + capabilities.add(capabilitySet); + } + else + { + throw new AlfrescoRuntimeException("Unable to find the capability set '" + capabilitySet + "'"); + } + } + + return capabilityService.getCapabilitiesAccessState(nodeRef, capabilities); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService#getCapability(java.lang.String) + */ + public Capability getCapability(String name) + { + return capabilityService.getCapability(name); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService#getProtectedAspects() + */ + public Set getProtectedAspects() + { + return voter.getProtetcedAscpects(); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService#getProtectedProperties() + */ + public Set getProtectedProperties() + { + return voter.getProtectedProperties(); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService#bootstrapDefaultRoles(org.alfresco.service.cmr.repository.NodeRef) + */ + public void bootstrapDefaultRoles(final NodeRef rmRootNode) + { + AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public Object doWork() + { + try + { + JSONArray array = null; + try + { + // Load up the default roles from JSON + InputStream is = getClass().getClassLoader().getResourceAsStream("alfresco/module/org_alfresco_module_rm/security/rm-default-roles-bootstrap.json"); + if (is == null) + { + throw new AlfrescoRuntimeException("Could not load default bootstrap roles configuration"); + } + array = new JSONArray(convertStreamToString(is)); + } + catch (IOException ioe) + { + throw new AlfrescoRuntimeException("Unable to load rm-default-roles-bootstrap.json configuration file.", ioe); + } + + // Add each role to the rm root node + for (int i = 0; i < array.length(); i++) + { + JSONObject object = array.getJSONObject(i); + + // Get the name of the role + String name = null; + if (object.has("name") == true) + { + name = object.getString("name"); + if (existsRole(rmRootNode, name) == true) + { + throw new AlfrescoRuntimeException("The bootstrap role " + name + " already exists on the rm root node " + rmRootNode.toString()); + } + } + else + { + throw new AlfrescoRuntimeException("No name given to default bootstrap role. Check json configuration file."); + } + + + // Get the role's display label + String displayLabel = name; + if (object.has("displayLabel") == true) + { + displayLabel = object.getString("displayLabel"); + } + + // Determine whether the role is an admin role or not + boolean isAdmin = false; + if (object.has("isAdmin") == true) + { + isAdmin = object.getBoolean("isAdmin"); + } + + // Get the roles capabilities + Set capabilities = new HashSet(30); + if (object.has("capabilities") == true) + { + JSONArray arrCaps = object.getJSONArray("capabilities"); + for (int index = 0; index < arrCaps.length(); index++) + { + String capName = arrCaps.getString(index); + Capability capability = getCapability(capName); + if (capability == null) + { + throw new AlfrescoRuntimeException("The capability '" + capName + "' configured for the deafult boostrap role '" + name + "' is invalid."); + } + capabilities.add(capability); + } + } + + // Create the role + Role role = createRole(rmRootNode, name, displayLabel, capabilities); + + // Add any additional admin permissions + if (isAdmin == true) + { + permissionService.setPermission(rmRootNode, role.getRoleGroupName(), RMPermissionModel.FILING, true); + + // Add the owner of the root node into the admin group + //authorityService.addAuthority(role.getRoleGroupName(), ownableService.getOwner(rmRootNode)); + } + } + } + catch (JSONException exception) + { + throw new AlfrescoRuntimeException("Error loading json configuration file rm-default-roles-bootstrap.json", exception); + } + + return null; + } + }, AuthenticationUtil.getAdminUserName()); + } + + public String convertStreamToString(InputStream is) throws IOException + { + /* + * To convert the InputStream to String we use the BufferedReader.readLine() + * method. We iterate until the BufferedReader return null which means + * there's no more data to read. Each line will appended to a StringBuilder + * and returned as String. + */ + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + StringBuilder sb = new StringBuilder(); + + String line = null; + try + { + while ((line = reader.readLine()) != null) + { + sb.append(line + "\n"); + } + } + finally + { + try {is.close();} catch (IOException e) {} + } + + return sb.toString(); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService#getRoles() + */ + public Set getRoles(final NodeRef rmRootNode) + { + return AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork>() + { + public Set doWork() throws Exception + { + Set result = new HashSet(13); + + Set roleAuthorities = authorityService.getAllAuthoritiesInZone(getZoneName(rmRootNode), AuthorityType.GROUP); + for (String roleAuthority : roleAuthorities) + { + String name = getShortRoleName(authorityService.getShortName(roleAuthority), rmRootNode); + String displayLabel = authorityService.getAuthorityDisplayName(roleAuthority); + Set capabilities = getCapabilitiesImpl(rmRootNode, roleAuthority); + + Role role = new Role(name, displayLabel, capabilities, roleAuthority); + result.add(role); + } + + return result; + } + }, AuthenticationUtil.getAdminUserName()); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService#getRolesByUser(org.alfresco.service.cmr.repository.NodeRef, java.lang.String) + */ + public Set getRolesByUser(final NodeRef rmRootNode, final String user) + { + return AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork>() + { + public Set doWork() throws Exception + { + Set result = new HashSet(13); + + Set roleAuthorities = authorityService.getAllAuthoritiesInZone(getZoneName(rmRootNode), AuthorityType.GROUP); + for (String roleAuthority : roleAuthorities) + { + Set users = authorityService.getContainedAuthorities(AuthorityType.USER, roleAuthority, false); + if (users.contains(user) == true) + { + String name = getShortRoleName(authorityService.getShortName(roleAuthority), rmRootNode); + String displayLabel = authorityService.getAuthorityDisplayName(roleAuthority); + Set capabilities = getCapabilitiesImpl(rmRootNode, roleAuthority); + + Role role = new Role(name, displayLabel, capabilities, roleAuthority); + result.add(role); + } + } + + return result; + } + }, AuthenticationUtil.getAdminUserName()); + } + + /** + * + * @param rmRootNode + * @return + */ + private String getZoneName(NodeRef rmRootNode) + { + return RM_ROLE_ZONE_PREFIX + rmRootNode.getId(); + } + + /** + * Get the full role name + * + * @param role + * @param rmRootNode + * @return + */ + private String getFullRoleName(String role, NodeRef rmRootNode) + { + return role + rmRootNode.getId(); + } + + /** + * Get the short role name + * + * @param fullRoleName + * @param rmRootNode + * @return + */ + private String getShortRoleName(String fullRoleName, NodeRef rmRootNode) + { + return fullRoleName.replaceAll(rmRootNode.getId(), ""); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService#getRole(org.alfresco.service.cmr.repository.NodeRef, java.lang.String) + */ + public Role getRole(final NodeRef rmRootNode, final String role) + { + return AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public Role doWork() throws Exception + { + Role result = null; + + String roleAuthority = authorityService.getName(AuthorityType.GROUP, getFullRoleName(role, rmRootNode)); + if (authorityService.authorityExists(roleAuthority) == true) + { + String name = getShortRoleName(authorityService.getShortName(roleAuthority), rmRootNode); + String displayLabel = authorityService.getAuthorityDisplayName(roleAuthority); + Set capabilities = getCapabilitiesImpl(rmRootNode, roleAuthority); + + result = new Role(name, displayLabel, capabilities, roleAuthority); + } + + return result; + } + }, AuthenticationUtil.getAdminUserName()); + } + + private Set getCapabilitiesImpl(NodeRef rmRootNode, String roleAuthority) + { + Set permissions = permissionService.getAllSetPermissions(rmRootNode); + Set capabilities = new HashSet(52); + for (AccessPermission permission : permissions) + + { + if (permission.getAuthority().equals(roleAuthority) == true) + { + String capabilityName = permission.getPermission(); + if (getCapability(capabilityName) != null) + { + capabilities.add(permission.getPermission()); + } + } + + } + + return capabilities; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService#existsRole(java.lang.String) + */ + public boolean existsRole(final NodeRef rmRootNode, final String role) + { + return AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public Boolean doWork() throws Exception + { + String fullRoleName = authorityService.getName(AuthorityType.GROUP, getFullRoleName(role, rmRootNode)); + + String zone = getZoneName(rmRootNode); + Set roles = authorityService.getAllAuthoritiesInZone(zone, AuthorityType.GROUP); + return new Boolean(roles.contains(fullRoleName)); + } + }, AuthenticationUtil.getAdminUserName()).booleanValue(); + } + + /* + * @see org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService#hasRMAdminRole(org.alfresco.service.cmr.repository.NodeRef, java.lang.String) + */ + public boolean hasRMAdminRole(NodeRef rmRootNode, String user) + { + boolean isRMAdmin = false; + + Set userRoles = this.getRolesByUser(rmRootNode, user); + if (userRoles != null) + { + for (Role role : userRoles) + { + if (role.getName().equals("Administrator")) + { + isRMAdmin = true; + break; + } + } + } + + return isRMAdmin; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService#createRole(org.alfresco.service.cmr.repository.NodeRef, java.lang.String, java.lang.String, java.util.Set) + */ + public Role createRole(final NodeRef rmRootNode, final String role, final String roleDisplayLabel, final Set capabilities) + { + return AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public Role doWork() throws Exception + { + String fullRoleName = getFullRoleName(role, rmRootNode); + + // Check that the role does not already exist for the rm root node + if (authorityService.authorityExists(authorityService.getName(AuthorityType.GROUP, fullRoleName))) + { + throw new AlfrescoRuntimeException("The role " + role + " already exists for root rm node " + rmRootNode.getId()); + } + + // Create a group that relates to the records management role + Set zones = new HashSet(2); + zones.add(getZoneName(rmRootNode)); + zones.add(AuthorityService.ZONE_APP_DEFAULT); + String roleGroup = authorityService.createAuthority(AuthorityType.GROUP, fullRoleName, roleDisplayLabel, zones); + + // Add the roleGroup to the "all" role group + String allRoleGroup = authorityService.getName(AuthorityType.GROUP, getAllRolesGroupShortName(rmRootNode)); + authorityService.addAuthority(allRoleGroup, roleGroup); + + // Assign the various capabilities to the group on the root records management node + Set capStrings = new HashSet(53); + if (capabilities != null) + { + for (Capability capability : capabilities) + { + permissionService.setPermission(rmRootNode, roleGroup, capability.getName(), true); + } + + // Create the role + for (Capability capability : capabilities) + { + capStrings.add(capability.getName()); + } + } + + return new Role(role, roleDisplayLabel, capStrings, roleGroup); + } + }, AuthenticationUtil.getAdminUserName()); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService#updateRole(org.alfresco.service.cmr.repository.NodeRef, java.lang.String, java.lang.String, java.util.Set) + */ + public Role updateRole(final NodeRef rmRootNode, final String role, final String roleDisplayLabel, final Set capabilities) + { + return AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public Role doWork() throws Exception + { + String roleAuthority = authorityService.getName(AuthorityType.GROUP, getFullRoleName(role, rmRootNode)); + + // Reset the role display name + authorityService.setAuthorityDisplayName(roleAuthority, roleDisplayLabel); + + // TODO this needs to be improved, removing all and readding is not ideal + + // Clear the current capabilities + permissionService.clearPermission(rmRootNode, roleAuthority); + + // Re-add the provided capabilities + for (Capability capability : capabilities) + { + permissionService.setPermission(rmRootNode, roleAuthority, capability.getName(), true); + } + + Set capStrings = new HashSet(capabilities.size()); + for (Capability capability : capabilities) + { + capStrings.add(capability.getName()); + } + return new Role(role, roleDisplayLabel, capStrings, roleAuthority); + + } + }, AuthenticationUtil.getAdminUserName()); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService#deleteRole(java.lang.String) + */ + public void deleteRole(final NodeRef rmRootNode, final String role) + { + AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public Boolean doWork() throws Exception + { + String roleAuthority = authorityService.getName(AuthorityType.GROUP, getFullRoleName(role, rmRootNode)); + authorityService.deleteAuthority(roleAuthority); + return null; + + } + }, AuthenticationUtil.getAdminUserName()); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService#assignRoleToAuthority(org.alfresco.service.cmr.repository.NodeRef, java.lang.String, java.lang.String) + */ + public void assignRoleToAuthority(final NodeRef rmRootNode, final String role, final String authorityName) + { + AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public Boolean doWork() throws Exception + { + String roleAuthority = authorityService.getName(AuthorityType.GROUP, getFullRoleName(role, rmRootNode)); + authorityService.addAuthority(roleAuthority, authorityName); + return null; + + } + }, AuthenticationUtil.getAdminUserName()); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService#setPermission(org.alfresco.service.cmr.repository.NodeRef, java.lang.String, java.lang.String, boolean) + */ + public void setPermission(final NodeRef nodeRef, final String authority, final String permission) + { + ParameterCheck.mandatory("nodeRef", nodeRef); + ParameterCheck.mandatory("authority", authority); + ParameterCheck.mandatory("permission", permission); + + AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public Boolean doWork() throws Exception + { + if (recordsManagementService.isFilePlan(nodeRef) == false && + recordsManagementService.isRecordCategory(nodeRef) == true) + { + setReadPermissionUp(nodeRef, authority); + setPermissionDown(nodeRef, authority, permission); + } + else if (recordsManagementService.isRecordFolder(nodeRef) == true) + { + setReadPermissionUp(nodeRef, authority); + setPermissionImpl(nodeRef, authority, permission); + } + else + { + if (logger.isWarnEnabled() == true) + { + logger.warn("Setting permissions for this node is not supported. (nodeRef=" + nodeRef + ", authority=" + authority + ", permission=" + permission + ")"); + } + } + + return null; + } + }, AuthenticationUtil.getAdminUserName()); + } + + /** + * Helper method to set the read permission up the hierarchy + * + * @param nodeRef + * @param authority + */ + private void setReadPermissionUp(NodeRef nodeRef, String authority) + { + NodeRef parent = nodeService.getPrimaryParent(nodeRef).getParentRef(); + if (parent != null && + recordsManagementService.isFilePlan(parent) == false) + { + setPermissionImpl(parent, authority, RMPermissionModel.READ_RECORDS); + setReadPermissionUp(parent, authority); + } + } + + /** + * Helper method to set the permission down the hierarchy + * + * @param nodeRef + * @param authority + * @param permission + */ + private void setPermissionDown(NodeRef nodeRef, String authority, String permission) + { + setPermissionImpl(nodeRef, authority, permission); + if (recordsManagementService.isRecordCategory(nodeRef) == true) + { + List assocs = nodeService.getChildAssocs(nodeRef, ContentModel.ASSOC_CONTAINS, RegexQNamePattern.MATCH_ALL); + for (ChildAssociationRef assoc : assocs) + { + NodeRef child = assoc.getChildRef(); + if (recordsManagementService.isRecordCategory(child) == true || + recordsManagementService.isRecordFolder(child) == true) + { + setPermissionDown(child, authority, permission); + } + } + } + } + + /** + * Set the permission, taking into account that filing is a superset of read + * + * @param nodeRef + * @param authority + * @param permission + */ + private void setPermissionImpl(NodeRef nodeRef, String authority, String permission) + { + if (RMPermissionModel.FILING.equals(permission) == true) + { + // Remove record read permission before adding filing permission + permissionService.deletePermission(nodeRef, authority, RMPermissionModel.READ_RECORDS); + } + + permissionService.setPermission(nodeRef, authority, permission, true); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService#deletePermission(org.alfresco.service.cmr.repository.NodeRef, java.lang.String, java.lang.String) + */ + public void deletePermission(final NodeRef nodeRef, final String authority, final String permission) + { + AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public Boolean doWork() throws Exception + { + // Delete permission on this node + permissionService.deletePermission(nodeRef, authority, permission); + + if (recordsManagementService.isRecordCategory(nodeRef) == true) + { + List assocs = nodeService.getChildAssocs(nodeRef, ContentModel.ASSOC_CONTAINS, RegexQNamePattern.MATCH_ALL); + for (ChildAssociationRef assoc : assocs) + { + NodeRef child = assoc.getChildRef(); + if (recordsManagementService.isRecordCategory(child) == true || + recordsManagementService.isRecordFolder(child) == true) + { + deletePermission(child, authority, permission); + } + } + } + + return null; + } + }, AuthenticationUtil.getAdminUserName()); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/security/Role.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/security/Role.java new file mode 100644 index 0000000000..79b4986979 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/security/Role.java @@ -0,0 +1,79 @@ +/* + * 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.module.org_alfresco_module_rm.security; + +import java.util.Set; + +/** + * Records management role class + * + * @author Roy Wetherall + */ +public class Role +{ + private String name; + private String displayLabel; + private Set capabilities; + private String roleGroupName; + + /** + * @param name + * @param displayLabel + * @param capabilities + */ + public Role(String name, String displayLabel, Set capabilities, String roleGroupName) + { + this.name = name; + this.displayLabel = displayLabel; + this.capabilities = capabilities; + this.roleGroupName = roleGroupName; + } + + /** + * @return the name + */ + public String getName() + { + return name; + } + + /** + * @return the displayLabel + */ + public String getDisplayLabel() + { + return displayLabel; + } + + /** + * @return the capabilities + */ + public Set getCapabilities() + { + return capabilities; + } + + /** + * @return the roleGroupName + */ + public String getRoleGroupName() + { + return roleGroupName; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/CapabilitiesTestSuite.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/CapabilitiesTestSuite.java new file mode 100644 index 0000000000..9be78c443d --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/CapabilitiesTestSuite.java @@ -0,0 +1,47 @@ +/* + * 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.module.org_alfresco_module_rm.test; + +import junit.framework.Test; +import junit.framework.TestSuite; + +import org.alfresco.module.org_alfresco_module_rm.test.capabilities.CapabilitiesTest; +import org.alfresco.module.org_alfresco_module_rm.test.capabilities.DeclarativeCapabilityTest; + + +/** + * RM test suite + * + * @author Roy Wetherall + */ +public class CapabilitiesTestSuite extends TestSuite +{ + /** + * Creates the test suite + * + * @return the test suite + */ + public static Test suite() + { + TestSuite suite = new TestSuite(); + suite.addTestSuite(CapabilitiesTest.class); + suite.addTestSuite(DeclarativeCapabilityTest.class); + return suite; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/DOD5015Test.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/DOD5015Test.java new file mode 100644 index 0000000000..d32ee96e76 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/DOD5015Test.java @@ -0,0 +1,4582 @@ +/* + * 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.module.org_alfresco_module_rm.test; + +import java.io.File; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.transaction.UserTransaction; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminService; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionResult; +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService; +import org.alfresco.module.org_alfresco_module_rm.action.impl.BroadcastDispositionActionDefinitionUpdateAction; +import org.alfresco.module.org_alfresco_module_rm.action.impl.CompleteEventAction; +import org.alfresco.module.org_alfresco_module_rm.action.impl.EditDispositionActionAsOfDateAction; +import org.alfresco.module.org_alfresco_module_rm.action.impl.EditReviewAsOfDateAction; +import org.alfresco.module.org_alfresco_module_rm.action.impl.FileAction; +import org.alfresco.module.org_alfresco_module_rm.action.impl.FreezeAction; +import org.alfresco.module.org_alfresco_module_rm.capability.RMPermissionModel; +import org.alfresco.module.org_alfresco_module_rm.caveat.RMCaveatConfigService; +import org.alfresco.module.org_alfresco_module_rm.caveat.RMListOfValuesConstraint.MatchLogic; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionAction; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionActionDefinition; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService; +import org.alfresco.module.org_alfresco_module_rm.dod5015.DOD5015Model; +import org.alfresco.module.org_alfresco_module_rm.event.EventCompletionDetails; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementCustomModel; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementSearchBehaviour; +import org.alfresco.module.org_alfresco_module_rm.test.util.TestUtilities; +import org.alfresco.module.org_alfresco_module_rm.vital.VitalRecordDefinition; +import org.alfresco.module.org_alfresco_module_rm.vital.VitalRecordService; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.node.integrity.IntegrityException; +import org.alfresco.repo.search.impl.lucene.AbstractLuceneQueryParser; +import org.alfresco.repo.search.impl.lucene.fts.FullTextSearchIndexer; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.repo.security.permissions.AccessDeniedException; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +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; +import org.alfresco.service.cmr.repository.Period; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.AuthorityService; +import org.alfresco.service.cmr.security.AuthorityType; +import org.alfresco.service.cmr.security.MutableAuthenticationService; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.cmr.security.PublicServiceAccessService; +import org.alfresco.service.cmr.site.SiteVisibility; +import org.alfresco.service.cmr.view.ImporterService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.BaseSpringTest; +import org.alfresco.util.GUID; +import org.alfresco.util.PropertyMap; + +/** + * DOD System Test + * + * @author Roy Wetherall, Neil McErlean + */ +public class DOD5015Test extends BaseSpringTest implements RecordsManagementModel, DOD5015Model +{ + private static final Period weeklyReview = new Period("week|1"); + private static final Period dailyReview = new Period("day|1"); + public static final long TWENTY_FOUR_HOURS_IN_MS = 24 * 60 * 60 * 1000; // hours * minutes * seconds * millis + + protected static StoreRef SPACES_STORE = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "SpacesStore"); + + private NodeRef filePlan; + + private NodeService unprotectedNodeService; + private NodeService nodeService; + private SearchService searchService; + private ImporterService importService; + private ContentService contentService; + private RecordsManagementService rmService; + private RecordsManagementActionService rmActionService; + private ServiceRegistry serviceRegistry; + private TransactionService transactionService; + private RecordsManagementAdminService rmAdminService; + private RMCaveatConfigService caveatConfigService; + private DispositionService dispositionService; + private VitalRecordService vitalRecordService; + + private MutableAuthenticationService authenticationService; + private PersonService personService; + private AuthorityService authorityService; + private PermissionService permissionService; + private RetryingTransactionHelper transactionHelper; + + private PublicServiceAccessService publicServiceAccessService; + private FullTextSearchIndexer luceneFTS; + + // example base test data for supplemental markings list (see also recordsModel.xml) + protected final static String NOFORN = "NOFORN"; // Not Releasable to Foreign Nationals/Governments/Non-US Citizens + protected final static String NOCONTRACT = "NOCONTRACT"; // Not Releasable to Contractors or Contractor/Consultants + protected final static String FOUO = "FOUO"; // For Official Use Only + protected final static String FGI = "FGI"; // Foreign Government Information + + // example user-defined field + protected final static QName CONSTRAINT_CUSTOM_PRJLIST = QName.createQName(RM_CUSTOM_URI, "prjList"); + protected final static QName PROP_CUSTOM_PRJLIST = QName.createQName(RM_CUSTOM_URI, "projectNameList"); + + protected final static String PRJ_A = "Project A"; + protected final static String PRJ_B = "Project B"; + protected final static String PRJ_C = "Project C"; + + @Override + protected void onSetUpInTransaction() throws Exception + { + super.onSetUpInTransaction(); + + // Get the service required in the tests + this.unprotectedNodeService = (NodeService)applicationContext.getBean("nodeService"); + this.nodeService = (NodeService)this.applicationContext.getBean("NodeService"); // use upper 'N'odeService (to test access config interceptor) NodeService unprotectedNodeService = (NodeService)applicationContext.getBean("nodeService"); + this.authenticationService = (MutableAuthenticationService)this.applicationContext.getBean("AuthenticationService"); + this.personService = (PersonService)this.applicationContext.getBean("PersonService"); + this.authorityService = (AuthorityService)this.applicationContext.getBean("AuthorityService"); + this.permissionService = (PermissionService)this.applicationContext.getBean("PermissionService"); + this.searchService = (SearchService)this.applicationContext.getBean("SearchService"); // use upper 'S'earchService (to test access config interceptor) + this.importService = (ImporterService)this.applicationContext.getBean("importerComponent"); + this.contentService = (ContentService)this.applicationContext.getBean("ContentService"); + this.rmService = (RecordsManagementService)this.applicationContext.getBean("RecordsManagementService"); + this.rmActionService = (RecordsManagementActionService)this.applicationContext.getBean("RecordsManagementActionService"); + this.serviceRegistry = (ServiceRegistry)this.applicationContext.getBean("ServiceRegistry"); + this.transactionService = (TransactionService)this.applicationContext.getBean("TransactionService"); + this.rmAdminService = (RecordsManagementAdminService)this.applicationContext.getBean("RecordsManagementAdminService"); + this.caveatConfigService = (RMCaveatConfigService)this.applicationContext.getBean("caveatConfigService"); + this.publicServiceAccessService = (PublicServiceAccessService)this.applicationContext.getBean("PublicServiceAccessService"); + this.transactionHelper = (RetryingTransactionHelper)this.applicationContext.getBean("retryingTransactionHelper"); + this.dispositionService = (DispositionService)this.applicationContext.getBean("DispositionService"); + this.luceneFTS = (FullTextSearchIndexer)this.applicationContext.getBean("LuceneFullTextSearchIndexer"); + this.vitalRecordService = (VitalRecordService)applicationContext.getBean("VitalRecordService"); + + + // Set the current security context as admin + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + + // Get the test data + filePlan = TestUtilities.loadFilePlanData(applicationContext); + + File file = new File(System.getProperty("user.dir")+"/test-resources/testCaveatConfig1.json"); // from test-resources + assertTrue(file.exists()); + + caveatConfigService.updateOrCreateCaveatConfig(file); + + // set/reset allowed values (empty list by default) + List newValues = new ArrayList(4); + newValues.add(NOFORN); + newValues.add(NOCONTRACT); + newValues.add(FOUO); + newValues.add(FGI); + + rmAdminService.changeCustomConstraintValues(RecordsManagementCustomModel.CONSTRAINT_CUSTOM_SMLIST, newValues); + + // We pause FTS during this test, as it moves around records in intermediate places, and otherwise FTS may not + // finish clearing up its mess before each test finishes + this.luceneFTS.pause(); + } + + + + /* (non-Javadoc) + * @see org.springframework.test.AbstractTransactionalSpringContextTests#onTearDown() + */ + @Override + protected void onTearDown() throws Exception + { + super.onTearDown(); + + // Let FTS catch up again. + this.luceneFTS.resume(); + } + + + + /** + * Tests that the test data has been loaded correctly + */ + public void xtestTestData() throws Exception + { + // make sure the folders that should have disposition schedules do so + NodeRef janAuditRecordsFolder = TestUtilities.getRecordFolder(searchService, "Reports", "AIS Audit Records", "January AIS Audit Records"); + assertNotNull(janAuditRecordsFolder); + + // ensure the folder has the disposition lifecycle aspect + assertTrue("Expected 'January AIS Audit Records' folder to have disposition lifecycle aspect applied", + nodeService.hasAspect(janAuditRecordsFolder, ASPECT_DISPOSITION_LIFECYCLE)); + + // ensure the folder has the correctly setup search aspect + checkSearchAspect(janAuditRecordsFolder); + + // check another folder that has events as part of the disposition schedule + NodeRef equalOppCoordFolder = TestUtilities.getRecordFolder(searchService, "Military Files", "Personnel Security Program Records", "Equal Opportunity Coordinator"); + assertNotNull(equalOppCoordFolder); + assertTrue("Expected 'Equal Opportunity Coordinator' folder to have disposition lifecycle aspect applied", + nodeService.hasAspect(equalOppCoordFolder, ASPECT_DISPOSITION_LIFECYCLE)); + checkSearchAspect(equalOppCoordFolder); + } + + /** + * This test method creates a non-vital record and then moves it to a vital folder + * (triggering a refile) and then moves it a second time to another vital record + * having different metadata. + * + * Moving a Record within the FilePlan should trigger a "refile". Refiling a record + * will lead to the reconsideration of its disposition, vital and transfer/accession + * metadata, with potential changes therein. + */ + public void testMoveRefileRecord() throws Exception + { + // Commit in order to trigger the setUpRecordFolder behaviour + setComplete(); + endTransaction(); + + final NodeRef nonVitalFolder = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + // Create a record folder under a "non-vital" category + NodeRef nonVitalRecordCategory = TestUtilities.getRecordCategory(searchService, "Reports", "Unit Manning Documents"); + assertNotNull(nonVitalRecordCategory); + + return createRecFolderNode(nonVitalRecordCategory); + } + }); + + final NodeRef recordUnderTest = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + // Create a (non-vital) record under the above folder + NodeRef recordUnderTest = createRecordNode(nonVitalFolder); + + rmActionService.executeRecordsManagementAction(recordUnderTest, "file"); + + TestUtilities.declareRecord(recordUnderTest, unprotectedNodeService, rmActionService); + + return recordUnderTest; + } + }); + + final NodeRef vitalFolder =transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + // No need to commit the transaction here as the record is non-vital and + // there is no metadata to copy down. + + NodeRef vitalFolder = retrieveJanuaryAISVitalFolder(); + + // Move the non-vital record under the vital folder. + serviceRegistry.getFileFolderService().move(recordUnderTest, vitalFolder, null); + + return vitalFolder; + } + }); + + final NodeRef secondVitalFolder = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + // At this point, the formerly nonVitalRecord is now actually vital. + assertTrue("Expected record.", rmService.isRecord(recordUnderTest)); + assertTrue("Expected declared.", rmService.isRecordDeclared(recordUnderTest)); + + final VitalRecordDefinition recordVrd = vitalRecordService.getVitalRecordDefinition(recordUnderTest); + assertNotNull("Moved record should now have a Vital Rec Defn", recordVrd); + assertEquals("Moved record had wrong review period", + vitalRecordService.getVitalRecordDefinition(vitalFolder).getReviewPeriod(), recordVrd.getReviewPeriod()); + assertNotNull("Moved record should now have a review-as-of date", nodeService.getProperty(recordUnderTest, PROP_REVIEW_AS_OF)); + + // Create another folder with different vital/disposition instructions + //TODO Change disposition instructions + NodeRef vitalRecordCategory = TestUtilities.getRecordCategory(searchService, "Reports", "AIS Audit Records"); + assertNotNull(vitalRecordCategory); + return createRecFolderNode(vitalRecordCategory); + } + }); + + final Date reviewDate = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Date execute() throws Throwable + { + Map props = nodeService.getProperties(secondVitalFolder); + final Serializable secondVitalFolderReviewPeriod = props.get(PROP_REVIEW_PERIOD); + assertEquals("Unexpected review period.", weeklyReview, secondVitalFolderReviewPeriod); + + // We are changing the review period of this second record folder. + nodeService.setProperty(secondVitalFolder, PROP_REVIEW_PERIOD, dailyReview); + + Date reviewDate = (Date)nodeService.getProperty(recordUnderTest, PROP_REVIEW_AS_OF); + + // Move the newly vital record under the second vital folder. I expect the reviewPeriod + // for the record to be changed again. + serviceRegistry.getFileFolderService().move(recordUnderTest, secondVitalFolder, null); + + return reviewDate; + } + }); + + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + Period newReviewPeriod = vitalRecordService.getVitalRecordDefinition(recordUnderTest).getReviewPeriod(); + assertEquals("Unexpected review period.", dailyReview, newReviewPeriod); + + Date updatedReviewDate = (Date)nodeService.getProperty(recordUnderTest, PROP_REVIEW_AS_OF); + // The reviewAsOf date should have changed to "24 hours from now". + assertFalse("reviewAsOf date was unchanged", reviewDate.equals(updatedReviewDate)); + long millisecondsUntilNextReview = updatedReviewDate.getTime() - new Date().getTime(); + assertTrue("new reviewAsOf date was not within 24 hours of now.", + millisecondsUntilNextReview <= TWENTY_FOUR_HOURS_IN_MS); + + nodeService.deleteNode(recordUnderTest); + nodeService.deleteNode(nonVitalFolder); + nodeService.deleteNode(secondVitalFolder); + + return null; + } + }); + } + + public void off_testMoveRefileRecordFolder() throws Exception + { + //TODO Impl me + fail("Not yet impl'd."); + } + + public void off_testCopyRefileRecordFolder() throws Exception + { + //TODO Impl me + fail("Not yet impl'd."); + } + + public void off_testCopyRefileRecord() throws Exception + { + //TODO Impl me + fail("Not yet impl'd."); + } + + private NodeRef createRecordCategoryNode(NodeRef parentRecordSeries) + { + NodeRef newCategory = this.nodeService.createNode(parentRecordSeries, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "Test category " + System.currentTimeMillis()), + TYPE_RECORD_CATEGORY).getChildRef(); + + return newCategory; + } + + private NodeRef createRecFolderNode(NodeRef parentRecordCategory) + { + NodeRef newFolder = this.nodeService.createNode(parentRecordCategory, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "Test folder " + System.currentTimeMillis()), + TYPE_RECORD_FOLDER).getChildRef(); + return newFolder; + } + + private NodeRef createRecordNode(NodeRef parentFolder) + { + NodeRef newRecord = this.nodeService.createNode(parentFolder, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, + "Record" + System.currentTimeMillis() + ".txt"), + ContentModel.TYPE_CONTENT).getChildRef(); + ContentWriter writer = this.contentService.getWriter(newRecord, ContentModel.PROP_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.setEncoding("UTF-8"); + writer.putContent("Irrelevant content"); + return newRecord; + } + + private NodeRef retrieveJanuaryAISVitalFolder() + { + final List resultNodeRefs = retrieveJanuaryAISVitalFolders(); + final int folderCount = resultNodeRefs.size(); +// assertTrue("There should only be one 'January AIS Audit Records' folder. Were " + folderCount, folderCount == 1); + + // This nodeRef should have rma:VRI=true, rma:reviewPeriod=week|1, rma:isClosed=false + return resultNodeRefs.get(0); + } + + private List retrieveJanuaryAISVitalFolders() + { + String typeQuery = "TYPE:\"" + TYPE_RECORD_FOLDER + "\" AND @cm\\:name:\"January AIS Audit Records\""; + ResultSet types = this.searchService.query(SPACES_STORE, SearchService.LANGUAGE_LUCENE, typeQuery); + + final List resultNodeRefs = types.getNodeRefs(); + types.close(); + return resultNodeRefs; + } + + /** + * Test duplicate id's + */ + public void xxtestDuplicateIDs() + { + List roots = rmService.getFilePlans(); + final NodeRef root = roots.get(0); + setComplete(); + endTransaction(); + + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + String name1 = GUID.generate(); + Map props = new HashMap(2); + props.put(ContentModel.PROP_NAME, name1); + props.put(PROP_IDENTIFIER, "bob"); + ChildAssociationRef assoc = nodeService.createNode( + root, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, name1), + TYPE_RECORD_CATEGORY, + props); + + return assoc.getChildRef(); + } + }); + + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + try + { + String name1 = GUID.generate(); + Map props = new HashMap(2); + props.put(ContentModel.PROP_NAME, name1); + props.put(PROP_IDENTIFIER, "bob"); + ChildAssociationRef assoc = nodeService.createNode( + root, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, name1), + TYPE_RECORD_CATEGORY, + props); + fail("Cant duplicate series id"); + } + catch (Exception e) + { + // expected + } + + return null; + } + }); + } + + public void testDispositionLifecycle_0318_01_basictest() throws Exception + { + final NodeRef recordCategory = TestUtilities.getRecordCategory(searchService, "Reports", "AIS Audit Records"); + setComplete(); + endTransaction(); + + final NodeRef recordFolder = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + + assertNotNull(recordCategory); + assertEquals("AIS Audit Records", nodeService.getProperty(recordCategory, ContentModel.PROP_NAME)); + + return createRecordFolder(recordCategory, "March AIS Audit Records"); + } + }); + + final NodeRef recordOne = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + // Check the folder to ensure everything has been inherited correctly + assertTrue(((Boolean)nodeService.getProperty(recordFolder, PROP_VITAL_RECORD_INDICATOR)).booleanValue()); + assertEquals(nodeService.getProperty(recordCategory, PROP_REVIEW_PERIOD), + nodeService.getProperty(recordFolder, PROP_REVIEW_PERIOD)); + + // Create the document + Map props = new HashMap(1); + props.put(ContentModel.PROP_NAME, "MyRecord.txt"); + NodeRef recordOne = nodeService.createNode(recordFolder, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "MyRecord.txt"), + ContentModel.TYPE_CONTENT).getChildRef(); + + // Set the content + ContentWriter writer = contentService.getWriter(recordOne, ContentModel.PROP_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.setEncoding("UTF-8"); + writer.putContent("There is some content in this record"); + + return recordOne; + } + }); + + // Checked that the document has been marked as incomplete + System.out.println("recordOne ..."); + + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + assertTrue(nodeService.hasAspect(recordOne, ASPECT_RECORD)); + assertNotNull(nodeService.getProperty(recordOne, PROP_IDENTIFIER)); + System.out.println("Record id: " + nodeService.getProperty(recordOne, PROP_IDENTIFIER)); + assertNotNull(nodeService.getProperty(recordOne, PROP_DATE_FILED)); + System.out.println("Date filed: " + nodeService.getProperty(recordOne, PROP_DATE_FILED)); + + // Check the review schedule + assertTrue(nodeService.hasAspect(recordOne, ASPECT_VITAL_RECORD)); + assertNotNull(nodeService.getProperty(recordOne, PROP_REVIEW_AS_OF)); + System.out.println("Review as of: " + nodeService.getProperty(recordOne, PROP_REVIEW_AS_OF)); + + // Change the review asOf date + Date nowDate = new Date(); + assertFalse(nowDate.equals(nodeService.getProperty(recordOne, PROP_REVIEW_AS_OF))); + Map reviewAsOfParams = new HashMap(1); + reviewAsOfParams.put(EditReviewAsOfDateAction.PARAM_AS_OF_DATE, nowDate); + rmActionService.executeRecordsManagementAction(recordOne, "editReviewAsOfDate", reviewAsOfParams); + assertTrue(nowDate.equals(nodeService.getProperty(recordOne, PROP_REVIEW_AS_OF))); + + // NOTE the disposition is being managed at a folder level ... + + // Check the disposition action + assertFalse(nodeService.hasAspect(recordOne, ASPECT_DISPOSITION_LIFECYCLE)); + assertTrue(nodeService.hasAspect(recordFolder, ASPECT_DISPOSITION_LIFECYCLE)); + + NodeRef ndNodeRef = nodeService.getChildAssocs(recordFolder, ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + assertNotNull(ndNodeRef); + + assertNotNull(nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_ACTION_ID)); + System.out.println("Disposition action id: " + nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_ACTION_ID)); + assertEquals("cutoff", nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_ACTION)); + System.out.println("Disposition action: " + nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_ACTION)); + assertNotNull(nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_AS_OF)); + System.out.println("Disposition as of: " + nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_AS_OF)); + + // Check for the search properties having been populated + checkSearchAspect(recordFolder); + + // Test the declaration of a record by editing properties + Map propValues = new HashMap(); + propValues.put(RecordsManagementModel.PROP_PUBLICATION_DATE, new Date()); + List smList = new ArrayList(2); + smList.add(FOUO); + smList.add(NOFORN); + propValues.put(RecordsManagementModel.PROP_SUPPLEMENTAL_MARKING_LIST, (Serializable)smList); + propValues.put(RecordsManagementModel.PROP_MEDIA_TYPE, "mediaTypeValue"); + propValues.put(RecordsManagementModel.PROP_FORMAT, "formatValue"); + propValues.put(RecordsManagementModel.PROP_DATE_RECEIVED, new Date()); + nodeService.addProperties(recordOne, propValues); + + return null; + } + }); + + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + // Try and declare, expected failure + try + { + rmActionService.executeRecordsManagementAction(recordOne, "declareRecord"); + fail("Should not be able to declare a record that still has mandatory properties unset"); + } + catch (Exception e) + { + // Expected + } + + return null; + } + }); + + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + @SuppressWarnings("deprecation") + public Object execute() throws Throwable + { + assertTrue("Before test DECLARED aspect was set", + nodeService.hasAspect(recordOne, ASPECT_DECLARED_RECORD) == false); + + nodeService.setProperty(recordOne, RecordsManagementModel.PROP_ORIGINATOR, "origValue"); + nodeService.setProperty(recordOne, RecordsManagementModel.PROP_ORIGINATING_ORGANIZATION, "origOrgValue"); + nodeService.setProperty(recordOne, ContentModel.PROP_TITLE, "titleValue"); + + // Declare the record as we have set everything we should have + rmActionService.executeRecordsManagementAction(recordOne, "declareRecord"); + assertTrue(" the record is not declared", nodeService.hasAspect(recordOne, ASPECT_DECLARED_RECORD)); + + // check that the declaredAt and declaredBy properties are set + assertNotNull(nodeService.getProperty(recordOne, PROP_DECLARED_BY)); + assertEquals("admin", nodeService.getProperty(recordOne, PROP_DECLARED_BY)); + assertNotNull(nodeService.getProperty(recordOne, PROP_DECLARED_AT)); + Date dateNow = new Date(); + Date declaredDate = (Date)nodeService.getProperty(recordOne, PROP_DECLARED_AT); + assertEquals(declaredDate.getDate(), dateNow.getDate()); + assertEquals(declaredDate.getMonth(), dateNow.getMonth()); + assertEquals(declaredDate.getYear(), dateNow.getYear()); + + // Check that the history is empty + List history = dispositionService.getCompletedDispositionActions(recordFolder); + assertNotNull(history); + assertEquals(0, history.size()); + + return null; + } + }); + + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + // Execute the cutoff action (should fail because this is being done at the record level) + try + { + rmActionService.executeRecordsManagementAction(recordFolder, "cutoff", null); + fail(("Shouldn't have been able to execute cut off at the record level")); + } + catch (Exception e) + { + // expected + } + + // Execute the cutoff action (should fail becuase it is not yet eligiable) + try + { + rmActionService.executeRecordsManagementAction(recordFolder, "cutoff", null); + fail(("Shouldn't have been able to execute because it is not yet eligiable")); + } + catch (Exception e) + { + // expected + } + + return null; + } + }); + + final Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.HOUR, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + // Clock the asOf date back to ensure eligibility + NodeRef ndNodeRef = nodeService.getChildAssocs(recordFolder, ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + Date nowDate = calendar.getTime(); + assertFalse(nowDate.equals(nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_AS_OF))); + Map params = new HashMap(1); + params.put(EditDispositionActionAsOfDateAction.PARAM_AS_OF_DATE, nowDate); + rmActionService.executeRecordsManagementAction(recordFolder, "editDispositionActionAsOfDate", params); + assertTrue(nowDate.equals(nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_AS_OF))); + + // Cut off + rmActionService.executeRecordsManagementAction(recordFolder, "cutoff", null); + + return null; + } + }); + + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + // Check the disposition action + assertFalse(nodeService.hasAspect(recordOne, ASPECT_DISPOSITION_LIFECYCLE)); + assertTrue(nodeService.hasAspect(recordFolder, ASPECT_DISPOSITION_LIFECYCLE)); + + NodeRef ndNodeRef = nodeService.getChildAssocs(recordFolder, ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + assertNotNull(ndNodeRef); + + assertNotNull(nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_ACTION_ID)); + System.out.println("Disposition action id: " + nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_ACTION_ID)); + assertEquals("destroy", nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_ACTION)); + System.out.println("Disposition action: " + nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_ACTION)); + assertNotNull(nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_AS_OF)); + System.out.println("Disposition as of: " + nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_AS_OF)); + assertNull(nodeService.getProperty(recordFolder, RecordsManagementSearchBehaviour.PROP_RS_DISPOSITION_EVENTS)); + + // Check the previous action details + checkLastDispositionAction(recordFolder, "cutoff", 1); + + // Check for the search properties having been populated + checkSearchAspect(recordFolder); + + // Clock the asOf date back to ensure eligibility + ndNodeRef = nodeService.getChildAssocs(recordFolder, ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + Date nowDate = calendar.getTime(); + assertFalse(nowDate.equals(nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_AS_OF))); + Map params = new HashMap(1); + params.put(EditDispositionActionAsOfDateAction.PARAM_AS_OF_DATE, nowDate); + rmActionService.executeRecordsManagementAction(recordFolder, "editDispositionActionAsOfDate", params); + assertTrue(nowDate.equals(nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_AS_OF))); + + rmActionService.executeRecordsManagementAction(recordFolder, "destroy", null); + + // Check that the node has been destroyed (ghosted) + //assertFalse(nodeService.exists(recordFolder)); + //assertFalse(nodeService.exists(recordOne)); + assertTrue(nodeService.hasAspect(recordFolder, ASPECT_GHOSTED)); + assertTrue(nodeService.hasAspect(recordOne, ASPECT_GHOSTED)); + + // Check the history + if (nodeService.exists(recordFolder) == true) + { + checkLastDispositionAction(recordFolder, "destroy", 2); + } + + return null; + } + }); + } + + /** + * Tests the re-scheduling of disposition lifecycles when the schedule changes + */ + public void testDispositionLifecycle_0318_reschedule_folderlevel() throws Exception + { + final NodeRef recordSeries = TestUtilities.getRecordSeries(searchService, "Reports"); + setComplete(); + endTransaction(); + + // create a category + final NodeRef recordCategory = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + assertNotNull(recordSeries); + assertEquals("Reports", nodeService.getProperty(recordSeries, ContentModel.PROP_NAME)); + + return createRecordCategoryNode(recordSeries); + } + }); + + // define the disposition schedule for the category (Cut off monthly, hold 1 month, then destroy) + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + assertNotNull(recordCategory); + + DispositionSchedule schedule = dispositionService.getDispositionSchedule(recordCategory); + assertNotNull(schedule); + nodeService.setProperty(schedule.getNodeRef(), PROP_DISPOSITION_INSTRUCTIONS, "Cutoff after 1 month then destroy after 1 month"); + nodeService.setProperty(schedule.getNodeRef(), PROP_DISPOSITION_AUTHORITY, "Alfresco"); + + // define properties for both steps + Map step1 = new HashMap(); + step1.put(RecordsManagementModel.PROP_DISPOSITION_ACTION_NAME, "cutoff"); + step1.put(RecordsManagementModel.PROP_DISPOSITION_DESCRIPTION, "Cutoff after 1 month"); + step1.put(RecordsManagementModel.PROP_DISPOSITION_PERIOD, "month|1"); + + Map step2 = new HashMap(); + step2.put(RecordsManagementModel.PROP_DISPOSITION_ACTION_NAME, "destroy"); + step2.put(RecordsManagementModel.PROP_DISPOSITION_DESCRIPTION, "Destroy after 1 month"); + step2.put(RecordsManagementModel.PROP_DISPOSITION_PERIOD, "month|1"); + step2.put(RecordsManagementModel.PROP_DISPOSITION_PERIOD_PROPERTY, PROP_CUT_OFF_DATE); + + // add the action definitions to the schedule + dispositionService.addDispositionActionDefinition(schedule, step1); + dispositionService.addDispositionActionDefinition(schedule, step2); + + return null; + } + }); + + // create a record folder + final NodeRef recordFolder = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + return createRecordFolder(recordCategory, "Folder1"); + } + }); + + // make sure the disposition lifecycle is present and correct + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + @SuppressWarnings("deprecation") + public Object execute() throws Throwable + { + assertNotNull(recordFolder); + + assertTrue(nodeService.hasAspect(recordFolder, ASPECT_DISPOSITION_LIFECYCLE)); + NodeRef ndNodeRef = nodeService.getChildAssocs(recordFolder, ASSOC_NEXT_DISPOSITION_ACTION, + RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + assertNotNull(ndNodeRef); + + assertNotNull(nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_ACTION_ID)); + System.out.println("Disposition action id: " + nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_ACTION_ID)); + assertEquals("cutoff", nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_ACTION)); + System.out.println("Disposition action: " + nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_ACTION)); + assertNotNull(nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_AS_OF)); + Date asOfDate = (Date)nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_AS_OF); + System.out.println("Disposition as of: " + asOfDate); + + // make sure the as of date is a month in the future + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.MONTH, 1); + int monthThen = cal.get(Calendar.MONTH); + assertEquals(asOfDate.getMonth(), monthThen);; + + // make sure there aren't any events + List events = nodeService.getChildAssocs(ndNodeRef, ASSOC_EVENT_EXECUTIONS, + RegexQNamePattern.MATCH_ALL); + assertEquals(0, events.size()); + + // Check for the search properties having been populated + checkSearchAspect(recordFolder); + + return null; + } + }); + + // change the period on the 1st step of the disposition schedule and make sure it perculates down + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + // define changes for schedule + Map changes = new HashMap(); + changes.put(RecordsManagementModel.PROP_DISPOSITION_PERIOD, "month|3"); + + // update the second dispostion action definition + DispositionSchedule schedule = dispositionService.getDispositionSchedule(recordCategory); + assertNotNull(schedule); + List actionDefs = schedule.getDispositionActionDefinitions(); + assertEquals(2, actionDefs.size()); + System.out.println("Adding 3 months to period for 1st step: " + actionDefs.get(0).getName()); + updateDispositionActionDefinition(schedule, actionDefs.get(0), changes); + + return null; + } + }); + + // make sure the disposition lifecycle asOf date has been updated + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + @SuppressWarnings("deprecation") + public Object execute() throws Throwable + { + assertTrue(nodeService.hasAspect(recordFolder, ASPECT_DISPOSITION_LIFECYCLE)); + NodeRef ndNodeRef = nodeService.getChildAssocs(recordFolder, ASSOC_NEXT_DISPOSITION_ACTION, + RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + assertNotNull(ndNodeRef); + + assertNotNull(nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_AS_OF)); + Date asOfDate = (Date)nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_AS_OF); + System.out.println("Disposition as of: " + asOfDate); + + // make sure the as of date is a month in the future + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.MONTH, 3); + System.out.println("Test date: " + calendar.getTime()); + Calendar asOfCalendar = Calendar.getInstance(); + asOfCalendar.setTime(asOfDate); + assertEquals(calendar.get(Calendar.MONTH), asOfCalendar.get(Calendar.MONTH)); + + // Check for the search properties having been populated + checkSearchAspect(recordFolder); + + return null; + } + }); + + // change the period on the 2nd step of the disposition schedule and make sure it DOES NOT perculate down + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + // define changes for schedule + Map changes = new HashMap(); + changes.put(RecordsManagementModel.PROP_DISPOSITION_PERIOD, "month|6"); + + // update the first dispostion action definition + DispositionSchedule schedule = dispositionService.getDispositionSchedule(recordCategory); + assertNotNull(schedule); + List actionDefs = schedule.getDispositionActionDefinitions(); + assertEquals(2, actionDefs.size()); + System.out.println("Adding 6 months to period for 2nd step: " + actionDefs.get(1).getName()); + updateDispositionActionDefinition(schedule, actionDefs.get(1), changes); + + return null; + } + }); + + // make sure the disposition lifecycle asOf date has NOT been updated as the period was + // changed for a step other than the current one + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + @SuppressWarnings("deprecation") + public Object execute() throws Throwable + { + assertTrue(nodeService.hasAspect(recordFolder, ASPECT_DISPOSITION_LIFECYCLE)); + NodeRef ndNodeRef = nodeService.getChildAssocs(recordFolder, ASSOC_NEXT_DISPOSITION_ACTION, + RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + assertNotNull(ndNodeRef); + + assertNotNull(nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_AS_OF)); + Date asOfDate = (Date)nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_AS_OF); + System.out.println("Disposition as of: " + asOfDate); + + // make sure the as of date is a month in the future + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.MONTH, 3); + assertEquals("Expecting the asOf date to be unchanged",asOfDate.getMonth(), calendar.get(Calendar.MONTH)); + + // Check for the search properties having been populated + checkSearchAspect(recordFolder); + + return null; + } + }); + + // change the disposition schedule to be event based rather than time based i.e. + // remove the period properties and supply 2 events in its place. + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + // define changes for schedule + Map changes = new HashMap(); + changes.put(RecordsManagementModel.PROP_DISPOSITION_PERIOD, null); + List events = new ArrayList(2); + events.add("no_longer_needed"); + events.add("case_complete"); + changes.put(RecordsManagementModel.PROP_DISPOSITION_EVENT, (Serializable)events); + + // update the first dispostion action definition + DispositionSchedule schedule = dispositionService.getDispositionSchedule(recordCategory); + assertNotNull(schedule); + List actionDefs = schedule.getDispositionActionDefinitions(); + assertEquals(2, actionDefs.size()); + System.out.println("Removing period and adding no_longer_needed and case_complete to 1st step: " + + actionDefs.get(0).getName()); + updateDispositionActionDefinition(schedule, actionDefs.get(0), changes); + + return null; + } + }); + + // make sure the disposition lifecycle asOf date has been reset and there are now + // events hanging off the nextdispositionaction node + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + assertTrue(nodeService.hasAspect(recordFolder, ASPECT_DISPOSITION_LIFECYCLE)); + NodeRef ndNodeRef = nodeService.getChildAssocs(recordFolder, ASSOC_NEXT_DISPOSITION_ACTION, + RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + assertNotNull(ndNodeRef); + + Date asOfDate = (Date)nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_AS_OF); + System.out.println("New disposition as of: " + asOfDate); + assertNull("Expecting asOfDate to be null", asOfDate); + + // make sure the 2 events are present + List events = nodeService.getChildAssocs(ndNodeRef, ASSOC_EVENT_EXECUTIONS, + RegexQNamePattern.MATCH_ALL); + assertEquals(2, events.size()); + NodeRef event1 = events.get(0).getChildRef(); + assertEquals("no_longer_needed", nodeService.getProperty(event1, PROP_EVENT_EXECUTION_NAME)); + NodeRef event2 = events.get(1).getChildRef(); + assertEquals("case_complete", nodeService.getProperty(event2, PROP_EVENT_EXECUTION_NAME)); + + // Check for the search properties having been populated + checkSearchAspect(recordFolder, false); + + return null; + } + }); + + // remove one of the events just added + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + // define changes for schedule + Map changes = new HashMap(); + List events = new ArrayList(2); + events.add("case_complete"); + changes.put(RecordsManagementModel.PROP_DISPOSITION_EVENT, (Serializable)events); + + // update the first dispostion action definition + DispositionSchedule schedule = dispositionService.getDispositionSchedule(recordCategory); + assertNotNull(schedule); + List actionDefs = schedule.getDispositionActionDefinitions(); + assertEquals(2, actionDefs.size()); + System.out.println("Removing no_longer_needed event from 1st step: " + + actionDefs.get(0).getName()); + updateDispositionActionDefinition(schedule, actionDefs.get(0), changes); + + return null; + } + }); + + // make sure the disposition lifecycle asOf date is still null and ensure there is only one event + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + assertTrue(nodeService.hasAspect(recordFolder, ASPECT_DISPOSITION_LIFECYCLE)); + NodeRef ndNodeRef = nodeService.getChildAssocs(recordFolder, ASSOC_NEXT_DISPOSITION_ACTION, + RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + assertNotNull(ndNodeRef); + + Date asOfDate = (Date)nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_AS_OF); + assertNull("Expecting asOfDate to be null", asOfDate); + + // make sure only 1 event is present + List events = nodeService.getChildAssocs(ndNodeRef, ASSOC_EVENT_EXECUTIONS, + RegexQNamePattern.MATCH_ALL); + assertEquals(1, events.size()); + NodeRef event = events.get(0).getChildRef(); + assertEquals("case_complete", nodeService.getProperty(event, PROP_EVENT_EXECUTION_NAME)); + + // Check for the search properties having been populated + checkSearchAspect(recordFolder, false); + + return null; + } + }); + } + + /** + * Tests the re-scheduling of disposition lifecycles when the schedule changes + */ + public void testDispositionLifecycle_0318_reschedule_recordlevel() throws Exception + { + final NodeRef recordSeries = TestUtilities.getRecordSeries(searchService, "Reports"); + setComplete(); + endTransaction(); + + // create a category + final NodeRef recordCategory = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + assertNotNull(recordSeries); + assertEquals("Reports", nodeService.getProperty(recordSeries, ContentModel.PROP_NAME)); + + return createRecordCategoryNode(recordSeries); + } + }); + + // define the disposition schedule for the category (Cut off monthly, hold 1 month, then destroy) + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + assertNotNull(recordCategory); + + // get the disposition schedule and turn on record level disposition + DispositionSchedule schedule = dispositionService.getDispositionSchedule(recordCategory); + assertNotNull(schedule); + nodeService.setProperty(schedule.getNodeRef(), PROP_DISPOSITION_INSTRUCTIONS, "Cutoff after 1 month then destroy after 1 month"); + nodeService.setProperty(schedule.getNodeRef(), PROP_DISPOSITION_AUTHORITY, "Alfresco"); + nodeService.setProperty(schedule.getNodeRef(), PROP_RECORD_LEVEL_DISPOSITION, true); + + // define properties for both steps + Map step1 = new HashMap(); + step1.put(RecordsManagementModel.PROP_DISPOSITION_ACTION_NAME, "cutoff"); + step1.put(RecordsManagementModel.PROP_DISPOSITION_DESCRIPTION, "Cutoff after 1 month"); + step1.put(RecordsManagementModel.PROP_DISPOSITION_PERIOD, "month|1"); + + Map step2 = new HashMap(); + step2.put(RecordsManagementModel.PROP_DISPOSITION_ACTION_NAME, "destroy"); + step2.put(RecordsManagementModel.PROP_DISPOSITION_DESCRIPTION, "Destroy after 1 month"); + step2.put(RecordsManagementModel.PROP_DISPOSITION_PERIOD, "month|1"); + step2.put(RecordsManagementModel.PROP_DISPOSITION_PERIOD_PROPERTY, PROP_CUT_OFF_DATE); + + // add the action definitions to the schedule + dispositionService.addDispositionActionDefinition(schedule, step1); + dispositionService.addDispositionActionDefinition(schedule, step2); + + return null; + } + }); + + // create a record folder + final NodeRef recordFolder = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + return createRecordFolder(recordCategory, "Folder1"); + } + }); + + // create a record + final NodeRef record = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + assertNotNull(recordFolder); + + // Create the document + Map props = new HashMap(1); + props.put(ContentModel.PROP_NAME, "MyRecord.txt"); + NodeRef record = nodeService.createNode(recordFolder, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "MyRecord.txt"), + ContentModel.TYPE_CONTENT).getChildRef(); + + // Set the content + ContentWriter writer = contentService.getWriter(record, ContentModel.PROP_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.setEncoding("UTF-8"); + writer.putContent("There is some content in this record"); + + return record; + } + }); + + // make sure the disposition lifecycle is present and correct on the record and not on the folder + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + @SuppressWarnings("deprecation") + public Object execute() throws Throwable + { + assertNotNull(record); + + assertFalse(nodeService.hasAspect(recordFolder, ASPECT_DISPOSITION_LIFECYCLE)); + assertTrue(nodeService.hasAspect(record, ASPECT_DISPOSITION_LIFECYCLE)); + NodeRef ndNodeRef = nodeService.getChildAssocs(record, ASSOC_NEXT_DISPOSITION_ACTION, + RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + assertNotNull(ndNodeRef); + + assertNotNull(nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_ACTION_ID)); + System.out.println("Disposition action id: " + nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_ACTION_ID)); + assertEquals("cutoff", nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_ACTION)); + System.out.println("Disposition action: " + nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_ACTION)); + assertNotNull(nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_AS_OF)); + Date asOfDate = (Date)nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_AS_OF); + System.out.println("Disposition as of: " + asOfDate); + + // make sure the as of date is a month in the future + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.MONTH, 1); + int monthThen = cal.get(Calendar.MONTH); + assertEquals(asOfDate.getMonth(), monthThen); + + // make sure there aren't any events + List events = nodeService.getChildAssocs(ndNodeRef, ASSOC_EVENT_EXECUTIONS, + RegexQNamePattern.MATCH_ALL); + assertEquals(0, events.size()); + + // Check for the search properties having been populated + checkSearchAspect(record); + + return null; + } + }); + + // change the period on the 1st step of the disposition schedule and make sure it perculates down + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + // define changes for schedule + Map changes = new HashMap(); + changes.put(RecordsManagementModel.PROP_DISPOSITION_PERIOD, "month|3"); + + // update the second dispostion action definition + DispositionSchedule schedule = dispositionService.getDispositionSchedule(recordCategory); + assertNotNull(schedule); + List actionDefs = schedule.getDispositionActionDefinitions(); + assertEquals(2, actionDefs.size()); + System.out.println("Adding 3 months to period for 1st step: " + actionDefs.get(0).getName()); + updateDispositionActionDefinition(schedule, actionDefs.get(0), changes); + + return null; + } + }); + + // make sure the disposition lifecycle asOf date has been updated + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + @SuppressWarnings("deprecation") + public Object execute() throws Throwable + { + assertTrue(nodeService.hasAspect(record, ASPECT_DISPOSITION_LIFECYCLE)); + NodeRef ndNodeRef = nodeService.getChildAssocs(record, ASSOC_NEXT_DISPOSITION_ACTION, + RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + assertNotNull(ndNodeRef); + + assertNotNull(nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_AS_OF)); + Date asOfDate = (Date)nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_AS_OF); + System.out.println("Disposition as of: " + asOfDate); + + // make sure the as of date is a month in the future + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.MONTH, 3); + assertEquals(asOfDate.getMonth(), calendar.get(Calendar.MONTH)); + + // Check for the search properties having been populated + checkSearchAspect(record); + + return null; + } + }); + + // change the period on the 2nd step of the disposition schedule and make sure it DOES NOT perculate down + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + // define changes for schedule + Map changes = new HashMap(); + changes.put(RecordsManagementModel.PROP_DISPOSITION_PERIOD, "month|6"); + + // update the first dispostion action definition + DispositionSchedule schedule = dispositionService.getDispositionSchedule(recordCategory); + assertNotNull(schedule); + List actionDefs = schedule.getDispositionActionDefinitions(); + assertEquals(2, actionDefs.size()); + System.out.println("Adding 6 months to period for 2nd step: " + actionDefs.get(1).getName()); + updateDispositionActionDefinition(schedule, actionDefs.get(1), changes); + + return null; + } + }); + + // make sure the disposition lifecycle asOf date has NOT been updated as the period was + // changed for a step other than the current one + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + @SuppressWarnings("deprecation") + public Object execute() throws Throwable + { + assertTrue(nodeService.hasAspect(record, ASPECT_DISPOSITION_LIFECYCLE)); + NodeRef ndNodeRef = nodeService.getChildAssocs(record, ASSOC_NEXT_DISPOSITION_ACTION, + RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + assertNotNull(ndNodeRef); + + assertNotNull(nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_AS_OF)); + Date asOfDate = (Date)nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_AS_OF); + System.out.println("Disposition as of: " + asOfDate); + + // make sure the as of date is a month in the future + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.MONTH, 3); + assertEquals("Expecting the asOf date to be unchanged", asOfDate.getMonth(), calendar.get(Calendar.MONTH)); + + // Check for the search properties having been populated + checkSearchAspect(record); + + return null; + } + }); + + // change the disposition schedule to be event based rather than time based i.e. + // remove the period properties and supply 2 events in its place. + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + // define changes for schedule + Map changes = new HashMap(); + changes.put(RecordsManagementModel.PROP_DISPOSITION_PERIOD, null); + List events = new ArrayList(2); + events.add("no_longer_needed"); + events.add("case_complete"); + changes.put(RecordsManagementModel.PROP_DISPOSITION_EVENT, (Serializable)events); + + // update the first dispostion action definition + DispositionSchedule schedule = dispositionService.getDispositionSchedule(recordCategory); + assertNotNull(schedule); + List actionDefs = schedule.getDispositionActionDefinitions(); + assertEquals(2, actionDefs.size()); + System.out.println("Removing period and adding no_longer_needed and case_complete to 1st step: " + + actionDefs.get(0).getName()); + updateDispositionActionDefinition(schedule, actionDefs.get(0), changes); + + return null; + } + }); + + // make sure the disposition lifecycle asOf date has been reset and there are now + // events hanging off the nextdispositionaction node + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + assertTrue(nodeService.hasAspect(record, ASPECT_DISPOSITION_LIFECYCLE)); + NodeRef ndNodeRef = nodeService.getChildAssocs(record, ASSOC_NEXT_DISPOSITION_ACTION, + RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + assertNotNull(ndNodeRef); + + Date asOfDate = (Date)nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_AS_OF); + System.out.println("New disposition as of: " + asOfDate); + assertNull("Expecting asOfDate to be null", asOfDate); + + // make sure the 2 events are present + List events = nodeService.getChildAssocs(ndNodeRef, ASSOC_EVENT_EXECUTIONS, + RegexQNamePattern.MATCH_ALL); + assertEquals(2, events.size()); + NodeRef event1 = events.get(0).getChildRef(); + assertEquals("no_longer_needed", nodeService.getProperty(event1, PROP_EVENT_EXECUTION_NAME)); + NodeRef event2 = events.get(1).getChildRef(); + assertEquals("case_complete", nodeService.getProperty(event2, PROP_EVENT_EXECUTION_NAME)); + + // Check for the search properties having been populated + checkSearchAspect(record, false); + + return null; + } + }); + + // remove one of the events just added + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + // define changes for schedule + Map changes = new HashMap(); + List events = new ArrayList(2); + events.add("case_complete"); + changes.put(RecordsManagementModel.PROP_DISPOSITION_EVENT, (Serializable)events); + + // update the first dispostion action definition + DispositionSchedule schedule = dispositionService.getDispositionSchedule(recordCategory); + assertNotNull(schedule); + List actionDefs = schedule.getDispositionActionDefinitions(); + assertEquals(2, actionDefs.size()); + System.out.println("Removing no_longer_needed event from 1st step: " + + actionDefs.get(0).getName()); + updateDispositionActionDefinition(schedule, actionDefs.get(0), changes); + + return null; + } + }); + + // make sure the disposition lifecycle asOf date is still null and ensure there is only one event + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + assertTrue(nodeService.hasAspect(record, ASPECT_DISPOSITION_LIFECYCLE)); + NodeRef ndNodeRef = nodeService.getChildAssocs(record, ASSOC_NEXT_DISPOSITION_ACTION, + RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + assertNotNull(ndNodeRef); + + Date asOfDate = (Date)nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_AS_OF); + assertNull("Expecting asOfDate to be null", asOfDate); + + // make sure only 1 event is present + List events = nodeService.getChildAssocs(ndNodeRef, ASSOC_EVENT_EXECUTIONS, + RegexQNamePattern.MATCH_ALL); + assertEquals(1, events.size()); + NodeRef event = events.get(0).getChildRef(); + assertEquals("case_complete", nodeService.getProperty(event, PROP_EVENT_EXECUTION_NAME)); + + // Check for the search properties having been populated + checkSearchAspect(record, false); + + return null; + } + }); + + // change the action on the first step from 'cutoff' to 'retain' + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + // define changes for schedule + Map changes = new HashMap(); + changes.put(RecordsManagementModel.PROP_DISPOSITION_ACTION_NAME, "retain"); + + // update the first dispostion action definition + DispositionSchedule schedule = dispositionService.getDispositionSchedule(recordCategory); + assertNotNull(schedule); + List actionDefs = schedule.getDispositionActionDefinitions(); + assertEquals(2, actionDefs.size()); + System.out.println("Changing action of 1st step from '" + + actionDefs.get(0).getName() + "' to 'retain'"); + updateDispositionActionDefinition(schedule, actionDefs.get(0), changes); + + return null; + } + }); + + // make sure the disposition lifecycle asOf date is still null, ensure there is still only one event + // and most importantly that the action name is now 'retain' + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + assertTrue(nodeService.hasAspect(record, ASPECT_DISPOSITION_LIFECYCLE)); + NodeRef ndNodeRef = nodeService.getChildAssocs(record, ASSOC_NEXT_DISPOSITION_ACTION, + RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + assertNotNull(ndNodeRef); + + Date asOfDate = (Date)nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_AS_OF); + assertNull("Expecting asOfDate to be null", asOfDate); + + // make sure only 1 event is present + List events = nodeService.getChildAssocs(ndNodeRef, ASSOC_EVENT_EXECUTIONS, + RegexQNamePattern.MATCH_ALL); + assertEquals(1, events.size()); + NodeRef event = events.get(0).getChildRef(); + assertEquals("case_complete", nodeService.getProperty(event, PROP_EVENT_EXECUTION_NAME)); + + String actionName = (String)nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_ACTION); + assertEquals("retain", actionName); + + // Check for the search properties having been populated + checkSearchAspect(record, false); + + return null; + } + }); + } + + private void updateDispositionActionDefinition(DispositionSchedule schedule, DispositionActionDefinition actionDefinition, Map actionDefinitionParams) + { + NodeRef nodeRef = actionDefinition.getNodeRef(); + Map before = nodeService.getProperties(nodeRef); + nodeService.addProperties(nodeRef, actionDefinitionParams); + Map after = nodeService.getProperties(nodeRef); + List updatedProps = determineChangedProps(before, after); + + refreshDispositionActionDefinition(nodeRef, updatedProps); + } + + private void refreshDispositionActionDefinition(NodeRef nodeRef, List updatedProps) + { + if (updatedProps != null) + { + Map params = new HashMap(); + params.put(BroadcastDispositionActionDefinitionUpdateAction.CHANGED_PROPERTIES, (Serializable)updatedProps); + rmActionService.executeRecordsManagementAction(nodeRef, BroadcastDispositionActionDefinitionUpdateAction.NAME, params); + } + + // Remove the unpublished update aspect + nodeService.removeAspect(nodeRef, ASPECT_UNPUBLISHED_UPDATE); + } + + private List determineChangedProps(Map oldProps, Map newProps) + { + List result = new ArrayList(); + for (QName qn : oldProps.keySet()) + { + if (newProps.get(qn) == null || + newProps.get(qn).equals(oldProps.get(qn)) == false) + { + result.add(qn); + } + } + for (QName qn : newProps.keySet()) + { + if (oldProps.get(qn) == null) + { + result.add(qn); + } + } + + return result; + } + + /** + * Tests the re-scheduling of disposition lifecycles when steps from the schedule are deleted + * (when using folder level disposition) + */ + public void testDispositionLifecycle_0318_reschedule_deletion_folderlevel() throws Exception + { + final NodeRef recordSeries = TestUtilities.getRecordSeries(searchService, "Reports"); + setComplete(); + endTransaction(); + + // create a category + final NodeRef recordCategory = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + assertNotNull(recordSeries); + assertEquals("Reports", nodeService.getProperty(recordSeries, ContentModel.PROP_NAME)); + + return createRecordCategoryNode(recordSeries); + } + }); + + // define the disposition schedule for the category with several steps + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + assertNotNull(recordCategory); + + // define properties for both steps + Map step1 = new HashMap(); + step1.put(RecordsManagementModel.PROP_DISPOSITION_ACTION_NAME, "cutoff"); + step1.put(RecordsManagementModel.PROP_DISPOSITION_DESCRIPTION, "Cutoff when no longer needed"); + step1.put(RecordsManagementModel.PROP_DISPOSITION_EVENT, "no_longer_needed"); + + Map step2 = new HashMap(); + step2.put(RecordsManagementModel.PROP_DISPOSITION_ACTION_NAME, "transfer"); + step2.put(RecordsManagementModel.PROP_DISPOSITION_DESCRIPTION, "Transfer after 1 month"); + step2.put(RecordsManagementModel.PROP_DISPOSITION_PERIOD, "month|1"); + step2.put(RecordsManagementModel.PROP_DISPOSITION_PERIOD_PROPERTY, PROP_DISPOSITION_AS_OF); + + Map step3 = new HashMap(); + step3.put(RecordsManagementModel.PROP_DISPOSITION_ACTION_NAME, "destroy"); + step3.put(RecordsManagementModel.PROP_DISPOSITION_DESCRIPTION, "Destroy after 1 year"); + step3.put(RecordsManagementModel.PROP_DISPOSITION_PERIOD, "year|1"); + step3.put(RecordsManagementModel.PROP_DISPOSITION_PERIOD_PROPERTY, PROP_DISPOSITION_AS_OF); + + // add the action definitions to the schedule + DispositionSchedule schedule = dispositionService.getDispositionSchedule(recordCategory); + assertNotNull(schedule); + dispositionService.addDispositionActionDefinition(schedule, step1); + dispositionService.addDispositionActionDefinition(schedule, step2); + dispositionService.addDispositionActionDefinition(schedule, step3); + + return null; + } + }); + + // create first record folder + final NodeRef recordFolder1 = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + return createRecordFolder(recordCategory, "Folder1"); + } + }); + + // create second record folder + final NodeRef recordFolder2 = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + return createRecordFolder(recordCategory, "Folder2"); + } + }); + + // make sure the disposition lifecycle is present and correct + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + assertNotNull(recordFolder1); + assertNotNull(recordFolder2); + + assertTrue(nodeService.hasAspect(recordFolder1, ASPECT_DISPOSITION_LIFECYCLE)); + assertTrue(nodeService.hasAspect(recordFolder2, ASPECT_DISPOSITION_LIFECYCLE)); + NodeRef folder1NextAction = nodeService.getChildAssocs(recordFolder1, ASSOC_NEXT_DISPOSITION_ACTION, + RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + assertNotNull(folder1NextAction); + NodeRef folder2NextAction = nodeService.getChildAssocs(recordFolder2, ASSOC_NEXT_DISPOSITION_ACTION, + RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + assertNotNull(folder2NextAction); + + // make sure both folders are on the cutoff step + assertEquals("cutoff", nodeService.getProperty(folder1NextAction, PROP_DISPOSITION_ACTION)); + assertEquals("cutoff", nodeService.getProperty(folder2NextAction, PROP_DISPOSITION_ACTION)); + + // make sure both folders have 1 event + assertEquals(1, nodeService.getChildAssocs(folder1NextAction, ASSOC_EVENT_EXECUTIONS, + RegexQNamePattern.MATCH_ALL).size()); + assertEquals(1, nodeService.getChildAssocs(folder2NextAction, ASSOC_EVENT_EXECUTIONS, + RegexQNamePattern.MATCH_ALL).size()); + + // move folder 2 onto next step + Map params = new HashMap(3); + params.put(CompleteEventAction.PARAM_EVENT_NAME, "no_longer_needed"); + params.put(CompleteEventAction.PARAM_EVENT_COMPLETED_AT, new Date()); + params.put(CompleteEventAction.PARAM_EVENT_COMPLETED_BY, "gavinc"); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + rmActionService.executeRecordsManagementAction(recordFolder2, "completeEvent", params); + rmActionService.executeRecordsManagementAction(recordFolder2, "cutoff"); + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + + return null; + } + }); + + // check the second folder is at step 2 and then attempt to remove a step from the disposition schedule + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + NodeRef folder2NextAction = nodeService.getChildAssocs(recordFolder2, ASSOC_NEXT_DISPOSITION_ACTION, + RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + assertNotNull(folder2NextAction); + assertEquals("transfer", (String)nodeService.getProperty(folder2NextAction, PROP_DISPOSITION_ACTION)); + + // check there are 3 steps to the schedule + DispositionSchedule schedule = dispositionService.getDispositionSchedule(recordCategory); + assertNotNull(schedule); + List actionDefs = schedule.getDispositionActionDefinitions(); + assertEquals(3, actionDefs.size()); + + // attempt to remove step 1 from the schedule + try + { + dispositionService.removeDispositionActionDefinition(schedule, actionDefs.get(0)); + fail("Expecting the step deletion to be unsuccessful as record folders are present"); + } + catch (AlfrescoRuntimeException are) + { + // expected as steps are present, deletion not allowed + } + + return null; + } + }); + + // remove both record folders + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + // remove record folders + nodeService.removeChild(recordCategory, recordFolder1); + nodeService.removeChild(recordCategory, recordFolder2); + return null; + } + }); + + // try removing last schedule step + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + // make sure there are 3 steps + DispositionSchedule schedule = dispositionService.getDispositionSchedule(recordCategory); + assertNotNull(schedule); + List actionDefs = schedule.getDispositionActionDefinitions(); + assertEquals(3, actionDefs.size()); + + // remove last step, should be successful this time + dispositionService.removeDispositionActionDefinition(schedule, actionDefs.get(2)); + + // make sure there are now 2 steps + schedule = dispositionService.getDispositionSchedule(recordCategory); + actionDefs = schedule.getDispositionActionDefinitions(); + assertEquals(2, actionDefs.size()); + + return null; + } + }); + + // *** NOTE: The commented out code below is potential tests for the step deletion behaviour *** + // *** we also need to add tests for deleting the step in the process where records or *** + // *** folders are on the last step i.e. what state should they be in if the last step *** + // *** is removed? *** + + /* + // check the second folder is at step 2 and remove the first step from the schedule + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + NodeRef folder2NextAction = nodeService.getChildAssocs(recordFolder2, ASSOC_NEXT_DISPOSITION_ACTION, + RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + assertNotNull(folder2NextAction); + assertEquals("transfer", (String)nodeService.getProperty(folder2NextAction, PROP_DISPOSITION_ACTION)); + + // remove step 1 from the schedule + DispositionSchedule schedule = rmService.getDispositionSchedule(recordCategory); + assertNotNull(schedule); + List actionDefs = schedule.getDispositionActionDefinitions(); + assertEquals(3, actionDefs.size()); + System.out.println("Removing schedule step 1 named: " + actionDefs.get(0).getName()); + rmService.removeDispositionActionDefinition(schedule, actionDefs.get(0)); + + return null; + } + }); + + // make sure the next action for folder 1 has moved on and folder 2 is unchanged, then delete last step + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + NodeRef folder1NextAction = nodeService.getChildAssocs(recordFolder1, ASSOC_NEXT_DISPOSITION_ACTION, + RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + assertNotNull(folder1NextAction); + NodeRef folder2NextAction = nodeService.getChildAssocs(recordFolder2, ASSOC_NEXT_DISPOSITION_ACTION, + RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + assertNotNull(folder2NextAction); + + // make sure both folders are on the cutoff step + assertEquals("transfer", nodeService.getProperty(folder1NextAction, PROP_DISPOSITION_ACTION)); + assertEquals("transfer", nodeService.getProperty(folder2NextAction, PROP_DISPOSITION_ACTION)); + + // Check for the search properties having been populated + checkSearchAspect(folder1NextAction); + checkSearchAspect(folder2NextAction); + + // remove the step in the last position from the schedule + DispositionSchedule schedule = rmService.getDispositionSchedule(recordCategory); + assertNotNull(schedule); + List actionDefs = schedule.getDispositionActionDefinitions(); + assertEquals(2, actionDefs.size()); + System.out.println("Removing schedule last step named: " + actionDefs.get(1).getName()); + rmService.removeDispositionActionDefinition(schedule, actionDefs.get(1)); + + return null; + } + }); + + // check there were no changes, then remove the only remaining step + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + NodeRef folder1NextAction = nodeService.getChildAssocs(recordFolder1, ASSOC_NEXT_DISPOSITION_ACTION, + RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + assertNotNull(folder1NextAction); + assertEquals("transfer", (String)nodeService.getProperty(folder1NextAction, PROP_DISPOSITION_ACTION)); + + NodeRef folder2NextAction = nodeService.getChildAssocs(recordFolder2, ASSOC_NEXT_DISPOSITION_ACTION, + RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + assertNotNull(folder2NextAction); + assertEquals("transfer", (String)nodeService.getProperty(folder2NextAction, PROP_DISPOSITION_ACTION)); + + // remove last remaining step from the schedule + DispositionSchedule schedule = rmService.getDispositionSchedule(recordCategory); + assertNotNull(schedule); + List actionDefs = schedule.getDispositionActionDefinitions(); + assertEquals(1, actionDefs.size()); + System.out.println("Removing last remaining schedule step named: " + actionDefs.get(0).getName()); + rmService.removeDispositionActionDefinition(schedule, actionDefs.get(0)); + + return null; + } + }); + + // check there are no schedule steps left and that both folders no longer have the disposition lifecycle aspect, + // then add a new step + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + assertEquals(0, nodeService.getChildAssocs(recordFolder1, ASSOC_NEXT_DISPOSITION_ACTION, + RegexQNamePattern.MATCH_ALL).size()); + assertEquals(0, nodeService.getChildAssocs(recordFolder2, ASSOC_NEXT_DISPOSITION_ACTION, + RegexQNamePattern.MATCH_ALL).size()); + assertFalse(nodeService.hasAspect(recordFolder1, ASPECT_DISPOSITION_LIFECYCLE)); + assertFalse(nodeService.hasAspect(recordFolder2, ASPECT_DISPOSITION_LIFECYCLE)); + + // ensure schedule is empty + DispositionSchedule schedule = rmService.getDispositionSchedule(recordCategory); + assertNotNull(schedule); + List actionDefs = schedule.getDispositionActionDefinitions(); + assertEquals(0, actionDefs.size()); + + // add a new step + Map step1 = new HashMap(); + step1.put(RecordsManagementModel.PROP_DISPOSITION_ACTION_NAME, "retain"); + step1.put(RecordsManagementModel.PROP_DISPOSITION_DESCRIPTION, "Retain for 25 years"); + step1.put(RecordsManagementModel.PROP_DISPOSITION_PERIOD, "year|25"); + rmService.addDispositionActionDefinition(schedule, step1); + + return null; + } + }); + + // check both folders now have the retain action + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + NodeRef folder1NextAction = nodeService.getChildAssocs(recordFolder1, ASSOC_NEXT_DISPOSITION_ACTION, + RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + assertNotNull(folder1NextAction); + assertEquals("retain", (String)nodeService.getProperty(folder1NextAction, PROP_DISPOSITION_ACTION)); + assertNotNull(nodeService.getProperty(folder1NextAction, PROP_DISPOSITION_AS_OF)); + + NodeRef folder2NextAction = nodeService.getChildAssocs(recordFolder2, ASSOC_NEXT_DISPOSITION_ACTION, + RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + assertNotNull(folder2NextAction); + assertEquals("retain", (String)nodeService.getProperty(folder2NextAction, PROP_DISPOSITION_ACTION)); + assertNotNull(nodeService.getProperty(folder2NextAction, PROP_DISPOSITION_AS_OF)); + + return null; + } + }); + */ + } + + /** + * Tests the re-scheduling of disposition lifecycles when steps from the schedule are deleted + * (when using record level disposition) + */ + public void testDispositionLifecycle_0318_reschedule_deletion_recordlevel() throws Exception + { + final NodeRef recordSeries = TestUtilities.getRecordSeries(searchService, "Reports"); + setComplete(); + endTransaction(); + + // create a category + final NodeRef recordCategory = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + assertNotNull(recordSeries); + assertEquals("Reports", nodeService.getProperty(recordSeries, ContentModel.PROP_NAME)); + + return createRecordCategoryNode(recordSeries); + } + }); + + // define the disposition schedule for the category with several steps + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + assertNotNull(recordCategory); + + // get the disposition schedule and turn on record level disposition + DispositionSchedule schedule = dispositionService.getDispositionSchedule(recordCategory); + assertNotNull(schedule); + nodeService.setProperty(schedule.getNodeRef(), PROP_RECORD_LEVEL_DISPOSITION, true); + + // define properties for both steps + Map step1 = new HashMap(); + step1.put(RecordsManagementModel.PROP_DISPOSITION_ACTION_NAME, "cutoff"); + step1.put(RecordsManagementModel.PROP_DISPOSITION_DESCRIPTION, "Cutoff when no longer needed"); + step1.put(RecordsManagementModel.PROP_DISPOSITION_EVENT, "no_longer_needed"); + + Map step2 = new HashMap(); + step2.put(RecordsManagementModel.PROP_DISPOSITION_ACTION_NAME, "transfer"); + step2.put(RecordsManagementModel.PROP_DISPOSITION_DESCRIPTION, "Transfer after 1 month"); + step2.put(RecordsManagementModel.PROP_DISPOSITION_PERIOD, "month|1"); + step2.put(RecordsManagementModel.PROP_DISPOSITION_PERIOD_PROPERTY, PROP_DISPOSITION_AS_OF); + + Map step3 = new HashMap(); + step3.put(RecordsManagementModel.PROP_DISPOSITION_ACTION_NAME, "destroy"); + step3.put(RecordsManagementModel.PROP_DISPOSITION_DESCRIPTION, "Destroy after 1 year"); + step3.put(RecordsManagementModel.PROP_DISPOSITION_PERIOD, "year|1"); + step3.put(RecordsManagementModel.PROP_DISPOSITION_PERIOD_PROPERTY, PROP_DISPOSITION_AS_OF); + + // add the action definitions to the schedule + dispositionService.addDispositionActionDefinition(schedule, step1); + dispositionService.addDispositionActionDefinition(schedule, step2); + dispositionService.addDispositionActionDefinition(schedule, step3); + + return null; + } + }); + + // create first record folder + final NodeRef recordFolder = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + return createRecordFolder(recordCategory, "Record Folder"); + } + }); + + // create a record + final NodeRef record = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + assertNotNull(recordFolder); + + // Create the document + Map props = new HashMap(1); + props.put(ContentModel.PROP_NAME, "MyRecord.txt"); + NodeRef record = nodeService.createNode(recordFolder, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "MyRecord.txt"), + ContentModel.TYPE_CONTENT).getChildRef(); + + // Set the content + ContentWriter writer = contentService.getWriter(record, ContentModel.PROP_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.setEncoding("UTF-8"); + writer.putContent("There is some content in this record"); + + return record; + } + }); + + // make sure the disposition lifecycle is present and correct + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + assertNotNull(record); + + assertTrue(nodeService.hasAspect(record, ASPECT_DISPOSITION_LIFECYCLE)); + NodeRef recordNextAction = nodeService.getChildAssocs(record, ASSOC_NEXT_DISPOSITION_ACTION, + RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + assertNotNull(recordNextAction); + + // make sure the record is on the cutoff step + assertEquals("cutoff", nodeService.getProperty(recordNextAction, PROP_DISPOSITION_ACTION)); + + // make sure the record has 1 event + assertEquals(1, nodeService.getChildAssocs(recordNextAction, ASSOC_EVENT_EXECUTIONS, + RegexQNamePattern.MATCH_ALL).size()); + + return null; + } + }); + + // check for steps in schedule then attempt to delete one + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + // check there are 3 steps to the schedule + DispositionSchedule schedule = dispositionService.getDispositionSchedule(recordCategory); + assertNotNull(schedule); + List actionDefs = schedule.getDispositionActionDefinitions(); + assertEquals(3, actionDefs.size()); + + // attempt to remove step 1 from the schedule + try + { + dispositionService.removeDispositionActionDefinition(schedule, actionDefs.get(0)); + fail("Expecting the step deletion to be unsuccessful as records are present"); + } + catch (AlfrescoRuntimeException are) + { + // expected as steps are present, deletion not allowed + } + + return null; + } + }); + + // remove the record (the folder can stay) + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + // remove record folders + nodeService.removeChild(recordFolder, record); + return null; + } + }); + + // try removing last schedule step + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + // make sure there are 3 steps + DispositionSchedule schedule = dispositionService.getDispositionSchedule(recordCategory); + assertNotNull(schedule); + List actionDefs = schedule.getDispositionActionDefinitions(); + assertEquals(3, actionDefs.size()); + + // remove last step, should be successful this time + dispositionService.removeDispositionActionDefinition(schedule, actionDefs.get(2)); + + // make sure there are now 2 steps + schedule = dispositionService.getDispositionSchedule(recordCategory); + actionDefs = schedule.getDispositionActionDefinitions(); + assertEquals(2, actionDefs.size()); + + return null; + } + }); + } + + /** + * test a dispostion schedule being setup after a record folder and record + */ + public void testDispositionLifecycle_0318_existingfolders() throws Exception + { + final NodeRef recordSeries = TestUtilities.getRecordSeries(searchService, "Reports"); + setComplete(); + endTransaction(); + + // create a category + final NodeRef recordCategory = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + assertNotNull(recordSeries); + assertEquals("Reports", nodeService.getProperty(recordSeries, ContentModel.PROP_NAME)); + + return createRecordCategoryNode(recordSeries); + } + }); + + // create a record folder + final NodeRef recordFolder = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + assertNotNull(recordCategory); + return createRecordFolder(recordCategory, "Folder1"); + } + }); + + // define the disposition schedule for the category (Cut off monthly, hold 1 month, then destroy) + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + assertNotNull(recordFolder); + + // define properties for both steps + Map step1 = new HashMap(); + step1.put(RecordsManagementModel.PROP_DISPOSITION_ACTION_NAME, "cutoff"); + step1.put(RecordsManagementModel.PROP_DISPOSITION_DESCRIPTION, "Cutoff after 1 month"); + step1.put(RecordsManagementModel.PROP_DISPOSITION_PERIOD, "month|1"); + + Map step2 = new HashMap(); + step2.put(RecordsManagementModel.PROP_DISPOSITION_ACTION_NAME, "destroy"); + step2.put(RecordsManagementModel.PROP_DISPOSITION_DESCRIPTION, "Destroy after 1 month"); + step2.put(RecordsManagementModel.PROP_DISPOSITION_PERIOD, "month|1"); + step2.put(RecordsManagementModel.PROP_DISPOSITION_PERIOD_PROPERTY, PROP_CUT_OFF_DATE); + + // add the action definitions to the schedule + DispositionSchedule schedule = dispositionService.getDispositionSchedule(recordCategory); + assertNotNull(schedule); + + NodeRef temp = dispositionService.addDispositionActionDefinition(schedule, step1).getNodeRef(); + List updatedProps = new ArrayList(step1.keySet()); + refreshDispositionActionDefinition(temp, updatedProps); + + temp = dispositionService.addDispositionActionDefinition(schedule, step2).getNodeRef(); + updatedProps = new ArrayList(step2.keySet()); + refreshDispositionActionDefinition(temp, updatedProps); + + return null; + } + }); + + // make sure the disposition lifecycle is present and correct + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + @SuppressWarnings("deprecation") + public Object execute() throws Throwable + { + assertNotNull(recordFolder); + + DispositionAction da = dispositionService.getNextDispositionAction(recordFolder); + assertNotNull(da); + + assertNotNull(da.getDispositionActionDefinition()); + assertNotNull(da.getDispositionActionDefinition().getId()); + assertEquals("cutoff", da.getName()); + Date asOfDate = da.getAsOfDate(); + assertNotNull(asOfDate); + + // make sure the as of date is a month in the future + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.MONTH, 1); + int monthThen = cal.get(Calendar.MONTH); + assertEquals(asOfDate.getMonth(), monthThen); + + // make sure there aren't any events + assertEquals(0, da.getEventCompletionDetails().size()); + + // Check for the search properties having been populated + checkSearchAspect(recordFolder); + + return null; + } + }); + } + + /** + * Test the updating of a disposition schedule using folder level disposition + */ + public void testFolderLevelDispositionScheduleUpdate() throws Exception + { + final NodeRef recordSeries = TestUtilities.getRecordSeries(searchService, "Reports"); + setComplete(); + endTransaction(); + + // create a category + final NodeRef recordCategory = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + assertNotNull(recordSeries); + assertEquals("Reports", nodeService.getProperty(recordSeries, ContentModel.PROP_NAME)); + + return createRecordCategoryNode(recordSeries); + } + }); + + // define the disposition schedule for the category (Cut off monthly, hold 1 month, then destroy) + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + assertNotNull(recordCategory); + + // get the disposition schedule and turn on record level disposition + DispositionSchedule schedule = dispositionService.getDispositionSchedule(recordCategory); + assertNotNull(schedule); + nodeService.setProperty(schedule.getNodeRef(), PROP_DISPOSITION_INSTRUCTIONS, "Cutoff after 1 month then destroy after 1 month"); + nodeService.setProperty(schedule.getNodeRef(), PROP_DISPOSITION_AUTHORITY, "Alfresco"); + + return null; + } + }); + + // create a record folder + final NodeRef recordFolder = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + return createRecordFolder(recordCategory, "Folder1"); + } + }); + + // check the created folder has the correctly populated search aspect, then update the schedule + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + // check the folder has the search aspect + assertNotNull(recordFolder); + checkSearchAspect(recordFolder, false); + + // update the disposition schedule + DispositionSchedule schedule = dispositionService.getDispositionSchedule(recordCategory); + assertNotNull(schedule); + nodeService.setProperty(schedule.getNodeRef(), PROP_DISPOSITION_INSTRUCTIONS, "Cutoff immediately when case is closed then destroy after 1 year"); + nodeService.setProperty(schedule.getNodeRef(), PROP_DISPOSITION_AUTHORITY, "DoD"); + + return null; + } + }); + + // check the search aspect has been kept in sync + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + // check the folder has the search aspect + checkSearchAspect(recordFolder, false); + + return null; + } + }); + } + + /** + * Test the updating of a disposition schedule using record level disposition + */ + public void testRecordLevelDispositionScheduleUpdate() throws Exception + { + final NodeRef recordSeries = TestUtilities.getRecordSeries(searchService, "Reports"); + setComplete(); + endTransaction(); + + // create a category + final NodeRef recordCategory = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + assertNotNull(recordSeries); + assertEquals("Reports", nodeService.getProperty(recordSeries, ContentModel.PROP_NAME)); + + return createRecordCategoryNode(recordSeries); + } + }); + + // define the disposition schedule for the category (Cut off monthly, hold 1 month, then destroy) + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + assertNotNull(recordCategory); + + // get the disposition schedule and turn on record level disposition + DispositionSchedule schedule = dispositionService.getDispositionSchedule(recordCategory); + assertNotNull(schedule); + nodeService.setProperty(schedule.getNodeRef(), PROP_DISPOSITION_INSTRUCTIONS, "Cutoff after 1 month then destroy after 1 month"); + nodeService.setProperty(schedule.getNodeRef(), PROP_DISPOSITION_AUTHORITY, "Alfresco"); + nodeService.setProperty(schedule.getNodeRef(), PROP_RECORD_LEVEL_DISPOSITION, true); + + return null; + } + }); + + // create a record folder + final NodeRef recordFolder = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + return createRecordFolder(recordCategory, "Folder1"); + } + }); + + // create a record + final NodeRef record = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + assertNotNull(recordFolder); + + // Create the document + Map props = new HashMap(1); + props.put(ContentModel.PROP_NAME, "MyRecord.txt"); + NodeRef record = nodeService.createNode(recordFolder, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "MyRecord.txt"), + ContentModel.TYPE_CONTENT).getChildRef(); + + // Set the content + ContentWriter writer = contentService.getWriter(record, ContentModel.PROP_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.setEncoding("UTF-8"); + writer.putContent("There is some content in this record"); + + return record; + } + }); + + // check the created folder has the correctly populated search aspect, then update the schedule + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + // check the record has the search aspect + assertNotNull(record); + checkSearchAspect(record, false); + + // update the disposition schedule + DispositionSchedule schedule = dispositionService.getDispositionSchedule(recordCategory); + assertNotNull(schedule); + nodeService.setProperty(schedule.getNodeRef(), PROP_DISPOSITION_INSTRUCTIONS, "Cutoff immediately when case is closed then destroy after 1 year"); + nodeService.setProperty(schedule.getNodeRef(), PROP_DISPOSITION_AUTHORITY, "DoD"); + + return null; + } + }); + + // check the search aspect has been kept in sync + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + // check the record has the search aspect + checkSearchAspect(record, false); + + return null; + } + }); + } + + public void testUnCutoff() + { + final NodeRef recordCategory = TestUtilities.getRecordCategory(searchService, "Reports", "AIS Audit Records"); + setComplete(); + endTransaction(); + + final NodeRef recordFolder = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + + assertNotNull(recordCategory); + assertEquals("AIS Audit Records", nodeService.getProperty(recordCategory, ContentModel.PROP_NAME)); + + return createRecordFolder(recordCategory, "March AIS Audit Records"); + } + }); + + final NodeRef recordOne = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + return createRecord(recordFolder); + } + }); + + final Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.HOUR, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + TestUtilities.declareRecord(recordOne, unprotectedNodeService, rmActionService); + + // Clock the asOf date back to ensure eligibility + NodeRef ndNodeRef = nodeService.getChildAssocs(recordFolder, ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + Date nowDate = calendar.getTime(); + assertFalse(nowDate.equals(nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_AS_OF))); + Map params = new HashMap(1); + params.put(EditDispositionActionAsOfDateAction.PARAM_AS_OF_DATE, nowDate); + rmActionService.executeRecordsManagementAction(recordFolder, "editDispositionActionAsOfDate", params); + assertTrue(nowDate.equals(nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_AS_OF))); + + // Cut off + rmActionService.executeRecordsManagementAction(recordFolder, "cutoff", null); + + // Check that everything appears to be cutoff + assertTrue(nodeService.hasAspect(recordFolder, ASPECT_CUT_OFF)); + List records = rmService.getRecords(recordFolder); + for (NodeRef record : records) + { + assertTrue(nodeService.hasAspect(record, ASPECT_CUT_OFF)); + } + DispositionAction da = dispositionService.getNextDispositionAction(recordFolder); + assertNotNull(da); + assertFalse("cutoff".equals(da.getName())); + checkLastDispositionAction(recordFolder, "cutoff", 1); + + // Revert the cutoff + rmActionService.executeRecordsManagementAction(recordFolder, "unCutoff", null); + + // Check that everything has been reverted + assertFalse(nodeService.hasAspect(recordFolder, ASPECT_CUT_OFF)); + records = rmService.getRecords(recordFolder); + for (NodeRef record : records) + { + assertFalse(nodeService.hasAspect(record, ASPECT_CUT_OFF)); + } + da = dispositionService.getNextDispositionAction(recordFolder); + assertNotNull(da); + assertTrue("cutoff".equals(da.getName())); + assertNull(da.getStartedAt()); + assertNull(da.getStartedBy()); + assertNull(da.getCompletedAt()); + assertNull(da.getCompletedBy()); + List history = dispositionService.getCompletedDispositionActions(recordFolder); + assertNotNull(history); + assertEquals(0, history.size()); + + return null; + } + }); + + } + + private void checkLastDispositionAction(NodeRef nodeRef, String daName, int expectedCount) + { + // Check the previous action details + List history = dispositionService.getCompletedDispositionActions(nodeRef); + assertNotNull(history); + assertEquals(expectedCount, history.size()); + DispositionAction lastDA = history.get(history.size()-1); + assertEquals(daName, lastDA.getName()); + assertNotNull(lastDA.getStartedAt()); + assertNotNull(lastDA.getStartedBy()); + assertNotNull(lastDA.getCompletedAt()); + assertNotNull(lastDA.getCompletedBy()); + // Check the "get last" method + lastDA = dispositionService.getLastCompletedDispostionAction(nodeRef); + assertEquals(daName, lastDA.getName()); + } + + public void testFreeze() throws Exception + { + final NodeRef recordCategory = TestUtilities.getRecordCategory(this.searchService, "Reports", "AIS Audit Records"); + assertNotNull(recordCategory); + assertEquals("AIS Audit Records", this.nodeService.getProperty(recordCategory, ContentModel.PROP_NAME)); + + // Before we start just remove any outstanding holds + final NodeRef rootNode = this.rmService.getFilePlan(recordCategory); + List tempAssocs = this.nodeService.getChildAssocs(rootNode, ASSOC_HOLDS, RegexQNamePattern.MATCH_ALL); + for (ChildAssociationRef tempAssoc : tempAssocs) + { + this.nodeService.deleteNode(tempAssoc.getChildRef()); + } + + final NodeRef recordFolder = createRecordFolder(recordCategory, "March AIS Audit Records"); + + setComplete(); + endTransaction(); + + final NodeRef recordOne = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + return createRecord(recordFolder, "one.txt"); + } + }); + final NodeRef recordTwo = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + return createRecord(recordFolder, "two.txt"); + } + }); + final NodeRef recordThree = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + return createRecord(recordFolder, "three.txt"); + } + }); + + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + assertTrue(nodeService.hasAspect(recordOne, ASPECT_RECORD)); + assertTrue(nodeService.hasAspect(recordOne, ASPECT_FILE_PLAN_COMPONENT)); + + // Freeze the record + Map params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "reason1"); + rmActionService.executeRecordsManagementAction(recordOne, "freeze", params); + + return null; + } + }); + + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + // Check the hold exists + List holdAssocs = nodeService.getChildAssocs(rootNode, ASSOC_HOLDS, RegexQNamePattern.MATCH_ALL); + assertNotNull(holdAssocs); + assertEquals(1, holdAssocs.size()); + NodeRef holdNodeRef = holdAssocs.get(0).getChildRef(); + assertEquals("reason1", nodeService.getProperty(holdNodeRef, PROP_HOLD_REASON)); + List freezeAssocs = nodeService.getChildAssocs(holdNodeRef); + assertNotNull(freezeAssocs); + assertEquals(1, freezeAssocs.size()); + + // Check the nodes are frozen + assertTrue(nodeService.hasAspect(recordOne, ASPECT_FROZEN)); + assertNotNull(nodeService.getProperty(recordOne, PROP_FROZEN_AT)); + assertNotNull(nodeService.getProperty(recordOne, PROP_FROZEN_BY)); + assertFalse(nodeService.hasAspect(recordTwo, ASPECT_FROZEN)); + assertFalse(nodeService.hasAspect(recordThree, ASPECT_FROZEN)); + + // check the records have the hold reason reflected on the search aspect + assertEquals("reason1", nodeService.getProperty(recordOne, RecordsManagementSearchBehaviour.PROP_RS_HOLD_REASON)); + assertNull(nodeService.getProperty(recordTwo, RecordsManagementSearchBehaviour.PROP_RS_HOLD_REASON)); + assertNull(nodeService.getProperty(recordThree, RecordsManagementSearchBehaviour.PROP_RS_HOLD_REASON)); + + // Update the freeze reason + Map params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "reason1changed"); + rmActionService.executeRecordsManagementAction(holdNodeRef, "editHoldReason", params); + + // Check the hold has been updated + String updatedHoldReason = (String)nodeService.getProperty(holdNodeRef, PROP_HOLD_REASON); + assertEquals("reason1changed", updatedHoldReason); + + return null; + } + }); + + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + // check the search fields on the records have also been updated + assertEquals("reason1changed", nodeService.getProperty(recordOne, RecordsManagementSearchBehaviour.PROP_RS_HOLD_REASON)); + assertNull(nodeService.getProperty(recordTwo, RecordsManagementSearchBehaviour.PROP_RS_HOLD_REASON)); + assertNull(nodeService.getProperty(recordThree, RecordsManagementSearchBehaviour.PROP_RS_HOLD_REASON)); + + // Freeze a number of records + Map params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "reason2"); + List records = new ArrayList(2); + records.add(recordOne); + records.add(recordTwo); + records.add(recordThree); + rmActionService.executeRecordsManagementAction(records, "freeze", params); + + return null; + } + }); + + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + // Check the holds exist + List holdAssocs = nodeService.getChildAssocs(rootNode, ASSOC_HOLDS, RegexQNamePattern.MATCH_ALL); + assertNotNull(holdAssocs); + assertEquals(2, holdAssocs.size()); + for (ChildAssociationRef holdAssoc : holdAssocs) + { + String reason = (String)nodeService.getProperty(holdAssoc.getChildRef(), PROP_HOLD_REASON); + if (reason.equals("reason2") == true) + { + List freezeAssocs = nodeService.getChildAssocs(holdAssoc.getChildRef()); + assertNotNull(freezeAssocs); + assertEquals(3, freezeAssocs.size()); + } + else if (reason.equals("reason1changed") == true) + { + List freezeAssocs = nodeService.getChildAssocs(holdAssoc.getChildRef()); + assertNotNull(freezeAssocs); + assertEquals(1, freezeAssocs.size()); + } + } + + // Check the nodes are frozen + final List testRecords = Arrays.asList(new NodeRef[]{recordOne, recordTwo, recordThree}); + for (NodeRef nr : testRecords) + { + assertTrue(nodeService.hasAspect(nr, ASPECT_FROZEN)); + assertNotNull(nodeService.getProperty(nr, PROP_FROZEN_AT)); + assertNotNull(nodeService.getProperty(nr, PROP_FROZEN_BY)); + assertNotNull(nodeService.getProperty(nr, RecordsManagementSearchBehaviour.PROP_RS_HOLD_REASON)); + } + + // Unfreeze a node + rmActionService.executeRecordsManagementAction(recordThree, "unfreeze"); + + return null; + } + }); + + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + // Check the holds + List holdAssocs = nodeService.getChildAssocs(rootNode, ASSOC_HOLDS, RegexQNamePattern.MATCH_ALL); + assertNotNull(holdAssocs); + assertEquals(2, holdAssocs.size()); + for (ChildAssociationRef holdAssoc : holdAssocs) + { + String reason = (String)nodeService.getProperty(holdAssoc.getChildRef(), PROP_HOLD_REASON); + if (reason.equals("reason2") == true) + { + List freezeAssocs = nodeService.getChildAssocs(holdAssoc.getChildRef()); + assertNotNull(freezeAssocs); + assertEquals(2, freezeAssocs.size()); + } + else if (reason.equals("reason1changed") == true) + { + List freezeAssocs = nodeService.getChildAssocs(holdAssoc.getChildRef()); + assertNotNull(freezeAssocs); + assertEquals(1, freezeAssocs.size()); + } + } + + // Check the nodes are frozen + assertTrue(nodeService.hasAspect(recordOne, ASPECT_FROZEN)); + assertNotNull(nodeService.getProperty(recordOne, PROP_FROZEN_AT)); + assertNotNull(nodeService.getProperty(recordOne, PROP_FROZEN_BY)); + assertEquals("reason2", nodeService.getProperty(recordOne, RecordsManagementSearchBehaviour.PROP_RS_HOLD_REASON)); + assertTrue(nodeService.hasAspect(recordTwo, ASPECT_FROZEN)); + assertNotNull(nodeService.getProperty(recordTwo, PROP_FROZEN_AT)); + assertNotNull(nodeService.getProperty(recordTwo, PROP_FROZEN_BY)); + assertEquals("reason2", nodeService.getProperty(recordTwo, RecordsManagementSearchBehaviour.PROP_RS_HOLD_REASON)); + assertFalse(nodeService.hasAspect(recordThree, ASPECT_FROZEN)); + assertNull(nodeService.getProperty(recordThree, RecordsManagementSearchBehaviour.PROP_RS_HOLD_REASON)); + + return null; + } + }); + + // Put the relinquish hold request into its own transaction + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + // Check the holds + List holdAssocs = nodeService.getChildAssocs(rootNode, ASSOC_HOLDS, RegexQNamePattern.MATCH_ALL); + assertNotNull(holdAssocs); + assertEquals(2, holdAssocs.size()); + // Relinquish the first hold + NodeRef holdNodeRef = holdAssocs.get(0).getChildRef(); + assertEquals("reason1changed", nodeService.getProperty(holdNodeRef, PROP_HOLD_REASON)); + + rmActionService.executeRecordsManagementAction(holdNodeRef, "relinquishHold"); + + // Check the holds + holdAssocs = nodeService.getChildAssocs(rootNode, ASSOC_HOLDS, RegexQNamePattern.MATCH_ALL); + assertNotNull(holdAssocs); + assertEquals(1, holdAssocs.size()); + holdNodeRef = holdAssocs.get(0).getChildRef(); + assertEquals("reason2", nodeService.getProperty(holdNodeRef, PROP_HOLD_REASON)); + List freezeAssocs = nodeService.getChildAssocs(holdNodeRef); + assertNotNull(freezeAssocs); + assertEquals(2, freezeAssocs.size()); + + return null; + } + }); + + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + // Check the nodes are frozen + assertTrue(nodeService.hasAspect(recordOne, ASPECT_FROZEN)); + assertNotNull(nodeService.getProperty(recordOne, PROP_FROZEN_AT)); + assertNotNull(nodeService.getProperty(recordOne, PROP_FROZEN_BY)); + // TODO: record one is still linked to a hold so should have the original hold reason + // on the search aspect but we're presuming just one hold for now so the search hold + // reason will remain unchanged + assertEquals("reason2", nodeService.getProperty(recordOne, RecordsManagementSearchBehaviour.PROP_RS_HOLD_REASON)); + assertTrue(nodeService.hasAspect(recordTwo, ASPECT_FROZEN)); + assertNotNull(nodeService.getProperty(recordTwo, PROP_FROZEN_AT)); + assertNotNull(nodeService.getProperty(recordTwo, PROP_FROZEN_BY)); + assertEquals("reason2", nodeService.getProperty(recordTwo, RecordsManagementSearchBehaviour.PROP_RS_HOLD_REASON)); + assertFalse(nodeService.hasAspect(recordThree, ASPECT_FROZEN)); + assertNull(nodeService.getProperty(recordThree, RecordsManagementSearchBehaviour.PROP_RS_HOLD_REASON)); + + // Unfreeze + rmActionService.executeRecordsManagementAction(recordOne, "unfreeze"); + + return null; + } + }); + + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + // Check the holds + List holdAssocs = nodeService.getChildAssocs(rootNode, ASSOC_HOLDS, RegexQNamePattern.MATCH_ALL); + assertNotNull(holdAssocs); + assertEquals(1, holdAssocs.size()); + NodeRef holdNodeRef = holdAssocs.get(0).getChildRef(); + assertEquals("reason2", nodeService.getProperty(holdNodeRef, PROP_HOLD_REASON)); + List freezeAssocs = nodeService.getChildAssocs(holdNodeRef); + assertNotNull(freezeAssocs); + assertEquals(1, freezeAssocs.size()); + + // Check the nodes are frozen + assertFalse(nodeService.hasAspect(recordOne, ASPECT_FROZEN)); + assertNull(nodeService.getProperty(recordOne, RecordsManagementSearchBehaviour.PROP_RS_HOLD_REASON)); + assertTrue(nodeService.hasAspect(recordTwo, ASPECT_FROZEN)); + assertNotNull(nodeService.getProperty(recordTwo, PROP_FROZEN_AT)); + assertNotNull(nodeService.getProperty(recordTwo, PROP_FROZEN_BY)); + assertEquals("reason2", nodeService.getProperty(recordTwo, RecordsManagementSearchBehaviour.PROP_RS_HOLD_REASON)); + assertFalse(nodeService.hasAspect(recordThree, ASPECT_FROZEN)); + assertNull(nodeService.getProperty(recordThree, RecordsManagementSearchBehaviour.PROP_RS_HOLD_REASON)); + + // Unfreeze + rmActionService.executeRecordsManagementAction(recordTwo, "unfreeze"); + + return null; + } + }); + + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + // Check the holds + List holdAssocs = nodeService.getChildAssocs(rootNode, ASSOC_HOLDS, RegexQNamePattern.MATCH_ALL); + assertNotNull(holdAssocs); + assertEquals(0, holdAssocs.size()); + + // Check the nodes are unfrozen + assertFalse(nodeService.hasAspect(recordOne, ASPECT_FROZEN)); + assertFalse(nodeService.hasAspect(recordTwo, ASPECT_FROZEN)); + assertFalse(nodeService.hasAspect(recordThree, ASPECT_FROZEN)); + + // check the search hold reason is null on all records + assertNull(nodeService.getProperty(recordOne, RecordsManagementSearchBehaviour.PROP_RS_HOLD_REASON)); + assertNull(nodeService.getProperty(recordTwo, RecordsManagementSearchBehaviour.PROP_RS_HOLD_REASON)); + assertNull(nodeService.getProperty(recordThree, RecordsManagementSearchBehaviour.PROP_RS_HOLD_REASON)); + + return null; + } + }); + } + + public void testAutoSuperseded() + { + final NodeRef recordCategory = TestUtilities.getRecordCategory(this.searchService, "Civilian Files", "Employee Performance File System Records"); + assertNotNull(recordCategory); + assertEquals("Employee Performance File System Records", this.nodeService.getProperty(recordCategory, ContentModel.PROP_NAME)); + + final NodeRef recordFolder = createRecordFolder(recordCategory, "Test Record Folder"); + + // Before we start just remove any outstanding transfers + final NodeRef rootNode = this.rmService.getFilePlan(recordCategory); + List tempAssocs = this.nodeService.getChildAssocs(rootNode, ASSOC_TRANSFERS, RegexQNamePattern.MATCH_ALL); + for (ChildAssociationRef tempAssoc : tempAssocs) + { + this.nodeService.deleteNode(tempAssoc.getChildRef()); + } + + setComplete(); + endTransaction(); + + final NodeRef recordOne = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + return createRecord(recordFolder, "one.txt"); + } + }); + final NodeRef recordTwo = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + return createRecord(recordFolder, "two.txt"); + } + }); + + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + assertTrue(nodeService.hasAspect(recordOne, ASPECT_RECORD)); + + TestUtilities.declareRecord(recordOne, unprotectedNodeService, rmActionService); + TestUtilities.declareRecord(recordTwo, unprotectedNodeService, rmActionService); + + return null; + } + }); + + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + + DispositionAction da = dispositionService.getNextDispositionAction(recordTwo); + assertNotNull(da); + assertEquals("cutoff", da.getName()); + assertFalse(da.isEventsEligible()); + List events = da.getEventCompletionDetails(); + assertNotNull(events); + assertEquals(1, events.size()); + EventCompletionDetails event = events.get(0); + assertEquals("superseded", event.getEventName()); + assertFalse(event.isEventComplete()); + assertNull(event.getEventCompletedAt()); + assertNull(event.getEventCompletedBy()); + + rmAdminService.addCustomReference(recordOne, recordTwo, QName.createQName(RecordsManagementCustomModel.RM_CUSTOM_URI, "supersedes")); + + return null; + } + }); + + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + DispositionAction da = dispositionService.getNextDispositionAction(recordTwo); + assertNotNull(da); + assertEquals("cutoff", da.getName()); + assertTrue(da.isEventsEligible()); + List events = da.getEventCompletionDetails(); + assertNotNull(events); + assertEquals(1, events.size()); + EventCompletionDetails event = events.get(0); + assertEquals("superseded", event.getEventName()); + assertTrue(event.isEventComplete()); + assertNotNull(event.getEventCompletedAt()); + assertNotNull(event.getEventCompletedBy()); + + return null; + } + }); + } + + public void testVersioned() + { + final NodeRef recordCategory = TestUtilities.getRecordCategory(this.searchService, "Civilian Files", "Employee Performance File System Records"); + assertNotNull(recordCategory); + assertEquals("Employee Performance File System Records", this.nodeService.getProperty(recordCategory, ContentModel.PROP_NAME)); + + final NodeRef recordFolder = createRecordFolder(recordCategory, "Test Record Folder"); + + // Before we start just remove any outstanding transfers + final NodeRef rootNode = this.rmService.getFilePlan(recordCategory); + List tempAssocs = this.nodeService.getChildAssocs(rootNode, ASSOC_TRANSFERS, RegexQNamePattern.MATCH_ALL); + for (ChildAssociationRef tempAssoc : tempAssocs) + { + this.nodeService.deleteNode(tempAssoc.getChildRef()); + } + + setComplete(); + endTransaction(); + + final NodeRef recordOne = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + return createRecord(recordFolder, "one.txt"); + } + }); + final NodeRef recordTwo = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + return createRecord(recordFolder, "two.txt"); + } + }); + + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + assertTrue(nodeService.hasAspect(recordOne, ASPECT_RECORD)); + + TestUtilities.declareRecord(recordOne, unprotectedNodeService, rmActionService); + TestUtilities.declareRecord(recordTwo, unprotectedNodeService, rmActionService); + + assertFalse(nodeService.hasAspect(recordOne, ASPECT_VERSIONED_RECORD)); + + rmAdminService.addCustomReference(recordOne, recordTwo, QName.createQName(RecordsManagementCustomModel.RM_CUSTOM_URI, "versions")); + + return null; + } + }); + + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + assertTrue(nodeService.hasAspect(recordOne, ASPECT_VERSIONED_RECORD)); + + rmAdminService.removeCustomReference(recordOne, recordTwo, QName.createQName(RecordsManagementCustomModel.RM_CUSTOM_URI, "versions")); + + return null; + } + }); + + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + assertFalse(nodeService.hasAspect(recordOne, ASPECT_VERSIONED_RECORD)); + + return null; + } + }); + } + + public void testDispositionLifecycle_0430_02_transfer() throws Exception + { + final NodeRef recordCategory = TestUtilities.getRecordCategory(this.searchService, "Civilian Files", "Foreign Employee Award Files"); + assertNotNull(recordCategory); + assertEquals("Foreign Employee Award Files", this.nodeService.getProperty(recordCategory, ContentModel.PROP_NAME)); + + final NodeRef recordFolder = createRecordFolder(recordCategory, "Test Record Folder"); + + // Before we start just remove any outstanding transfers + final NodeRef rootNode = this.rmService.getFilePlan(recordCategory); + List tempAssocs = this.nodeService.getChildAssocs(rootNode, ASSOC_TRANSFERS, RegexQNamePattern.MATCH_ALL); + for (ChildAssociationRef tempAssoc : tempAssocs) + { + this.nodeService.deleteNode(tempAssoc.getChildRef()); + } + + setComplete(); + endTransaction(); + + final NodeRef recordOne = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + return createRecord(recordFolder, "one.txt"); + } + }); + final NodeRef recordTwo = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + return createRecord(recordFolder, "two.txt"); + } + }); + final NodeRef recordThree = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + // Create the document + Map props = new HashMap(1); + props.put(ContentModel.PROP_NAME, "three.pdf"); + NodeRef recordThree = nodeService.createNode(recordFolder, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "three.pdf"), + ContentModel.TYPE_CONTENT, + props).getChildRef(); + + // Set the content + ContentWriter writer = contentService.getWriter(recordOne, ContentModel.PROP_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_PDF); + writer.setEncoding("UTF-8"); + writer.putContent("asdas"); + + return recordThree; + } + }); + + final DispositionAction da = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public DispositionAction execute() throws Throwable + { + // Declare the records + TestUtilities.declareRecord(recordOne, unprotectedNodeService, rmActionService); + TestUtilities.declareRecord(recordTwo, unprotectedNodeService, rmActionService); + TestUtilities.declareRecord(recordThree, unprotectedNodeService, rmActionService); + + // Cutoff + Map params = new HashMap(3); + params.put(CompleteEventAction.PARAM_EVENT_NAME, "case_complete"); + params.put(CompleteEventAction.PARAM_EVENT_COMPLETED_AT, new Date()); + params.put(CompleteEventAction.PARAM_EVENT_COMPLETED_BY, "roy"); + rmActionService.executeRecordsManagementAction(recordFolder, "completeEvent", params); + rmActionService.executeRecordsManagementAction(recordFolder, "cutoff"); + + checkLastDispositionAction(recordFolder, "cutoff", 1); + + DispositionAction da = dispositionService.getNextDispositionAction(recordFolder); + assertNotNull(da); + assertEquals("transfer", da.getName()); + + assertFalse(nodeService.hasAspect(recordFolder, ASPECT_TRANSFERRED)); + + return da; + } + }); + + // Do the transfer + final Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.HOUR, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + + final Object actionResult = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + // Clock the asOf date back to ensure eligibility + Date nowDate = calendar.getTime(); + assertFalse(nowDate.equals(nodeService.getProperty(da.getNodeRef(), PROP_DISPOSITION_AS_OF))); + Map params = new HashMap(1); + params.put(EditDispositionActionAsOfDateAction.PARAM_AS_OF_DATE, nowDate); + rmActionService.executeRecordsManagementAction(recordFolder, "editDispositionActionAsOfDate", params); + assertTrue(nowDate.equals(nodeService.getProperty(da.getNodeRef(), PROP_DISPOSITION_AS_OF))); + + return rmActionService.executeRecordsManagementAction(recordFolder, "transfer", null); + } + }); + + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + assertFalse(nodeService.hasAspect(recordFolder, ASPECT_TRANSFERRED)); + assertFalse(nodeService.hasAspect(recordOne, ASPECT_TRANSFERRED)); + assertFalse(nodeService.hasAspect(recordTwo, ASPECT_TRANSFERRED)); + assertFalse(nodeService.hasAspect(recordThree, ASPECT_TRANSFERRED)); + + // Check that the next disposition action is still in the correct state + DispositionAction da = dispositionService.getNextDispositionAction(recordFolder); + assertNotNull(da); + assertEquals("transfer", da.getName()); + assertNotNull(da.getStartedAt()); + assertNotNull(da.getStartedBy()); + assertNull(da.getCompletedAt()); + assertNull(da.getCompletedBy()); + + checkLastDispositionAction(recordFolder, "cutoff", 1); + + // Check that the transfer object is created + assertNotNull(rootNode); + List assocs = nodeService.getChildAssocs(rootNode, ASSOC_TRANSFERS, RegexQNamePattern.MATCH_ALL); + assertNotNull(assocs); + assertEquals(1, assocs.size()); + NodeRef transferNodeRef = assocs.get(0).getChildRef(); + assertEquals(TYPE_TRANSFER, nodeService.getType(transferNodeRef)); + assertTrue(((Boolean)nodeService.getProperty(transferNodeRef, PROP_TRANSFER_PDF_INDICATOR)).booleanValue()); + assertEquals("Offline Storage", (String)nodeService.getProperty(transferNodeRef, PROP_TRANSFER_LOCATION)); + assertNotNull(actionResult); + assertEquals(transferNodeRef, ((RecordsManagementActionResult)actionResult).getValue()); + List children = nodeService.getChildAssocs(transferNodeRef, ASSOC_TRANSFERRED, RegexQNamePattern.MATCH_ALL); + assertNotNull(children); + assertEquals(1, children.size()); + + + // Complete the transfer + rmActionService.executeRecordsManagementAction(assocs.get(0).getChildRef(), "transferComplete"); + + // Check nodes have been marked correctly + assertTrue(nodeService.hasAspect(recordFolder, ASPECT_TRANSFERRED)); + assertTrue(nodeService.hasAspect(recordOne, ASPECT_TRANSFERRED)); + assertTrue(nodeService.hasAspect(recordTwo, ASPECT_TRANSFERRED)); + assertTrue(nodeService.hasAspect(recordThree, ASPECT_TRANSFERRED)); + + // Check the transfer object is deleted + assocs = nodeService.getChildAssocs(rootNode, ASSOC_TRANSFERS, RegexQNamePattern.MATCH_ALL); + assertNotNull(assocs); + assertEquals(0, assocs.size()); + + // Check the disposition action has been moved on + da = dispositionService.getNextDispositionAction(recordFolder); + assertNotNull(da); + assertEquals("transfer", da.getName()); + assertNull(da.getStartedAt()); + assertNull(da.getStartedBy()); + assertNull(da.getCompletedAt()); + assertNull(da.getCompletedBy()); + assertFalse(dispositionService.isNextDispositionActionEligible(recordFolder)); + + checkLastDispositionAction(recordFolder, "transfer", 2); + + return null; + } + }); + } + + private void checkSearchAspect(NodeRef record) + { + checkSearchAspect(record, true); + } + + private void checkSearchAspect(NodeRef record, boolean isPeriodSet) + { + DispositionAction da = dispositionService.getNextDispositionAction(record); + if (da != null) + { + assertTrue(nodeService.hasAspect(record, RecordsManagementSearchBehaviour.ASPECT_RM_SEARCH)); + assertEquals(da.getName(), + nodeService.getProperty(record, RecordsManagementSearchBehaviour.PROP_RS_DISPOSITION_ACTION_NAME)); + assertEquals(da.getAsOfDate(), + nodeService.getProperty(record, RecordsManagementSearchBehaviour.PROP_RS_DISPOSITION_ACTION_AS_OF)); + assertEquals(nodeService.getProperty(da.getNodeRef(), PROP_DISPOSITION_EVENTS_ELIGIBLE), + nodeService.getProperty(record, RecordsManagementSearchBehaviour.PROP_RS_DISPOSITION_EVENTS_ELIGIBLE)); + + int eventCount = da.getEventCompletionDetails().size(); + Collection events = (Collection)nodeService.getProperty(record, RecordsManagementSearchBehaviour.PROP_RS_DISPOSITION_EVENTS); + if (eventCount == 0) + { + assertNull(events); + } + else + { + assertEquals(eventCount, events.size()); + } + + DispositionActionDefinition daDef = da.getDispositionActionDefinition(); + assertNotNull(daDef); + Period period = daDef.getPeriod(); + if (isPeriodSet) + { + assertNotNull(period); + assertEquals(period.getPeriodType(), nodeService.getProperty(record, RecordsManagementSearchBehaviour.PROP_RS_DISPOSITION_PERIOD)); + assertEquals(period.getExpression(), nodeService.getProperty(record, RecordsManagementSearchBehaviour.PROP_RS_DISPOSITION_PERIOD_EXPRESSION)); + } + else + { + assertNull(period); + assertNull(nodeService.getProperty(record, RecordsManagementSearchBehaviour.PROP_RS_DISPOSITION_PERIOD)); + assertNull(nodeService.getProperty(record, RecordsManagementSearchBehaviour.PROP_RS_DISPOSITION_PERIOD_EXPRESSION)); + } + } + + DispositionSchedule ds = dispositionService.getDispositionSchedule(record); + Boolean value = (Boolean)nodeService.getProperty(record, RecordsManagementSearchBehaviour.PROP_RS_HAS_DISPOITION_SCHEDULE); + String dsInstructions = (String)nodeService.getProperty(record, RecordsManagementSearchBehaviour.PROP_RS_DISPOITION_INSTRUCTIONS); + String dsAuthority = (String)nodeService.getProperty(record, RecordsManagementSearchBehaviour.PROP_RS_DISPOITION_AUTHORITY); + if (ds != null) + { + assertTrue(value); + assertEquals(ds.getDispositionInstructions(), dsInstructions); + assertEquals(ds.getDispositionAuthority(), dsAuthority); + } + else + { + assertFalse(value); + } + + VitalRecordDefinition vrd = vitalRecordService.getVitalRecordDefinition(record); + if (vrd == null) + { + assertNull(nodeService.getProperty(record, RecordsManagementSearchBehaviour.PROP_RS_VITAL_RECORD_REVIEW_PERIOD)); + assertNull(nodeService.getProperty(record, RecordsManagementSearchBehaviour.PROP_RS_VITAL_RECORD_REVIEW_PERIOD_EXPRESSION)); + } + else + { + assertEquals(vrd.getReviewPeriod().getPeriodType(), + nodeService.getProperty(record, RecordsManagementSearchBehaviour.PROP_RS_VITAL_RECORD_REVIEW_PERIOD)); + assertEquals(vrd.getReviewPeriod().getExpression(), + nodeService.getProperty(record, RecordsManagementSearchBehaviour.PROP_RS_VITAL_RECORD_REVIEW_PERIOD_EXPRESSION)); + } + } + + + public void testDispositionLifecycle_0430_01_recordleveldisposition() throws Exception + { + NodeRef recordCategory = TestUtilities.getRecordCategory(this.searchService, "Civilian Files", "Employee Performance File System Records"); + assertNotNull(recordCategory); + assertEquals("Employee Performance File System Records", this.nodeService.getProperty(recordCategory, ContentModel.PROP_NAME)); + + NodeRef recordFolder = createRecordFolder(recordCategory, "My Record Folder"); + + setComplete(); + endTransaction(); + + UserTransaction txn = transactionService.getUserTransaction(false); + txn.begin(); + + NodeRef recordOne = createRecord(recordFolder, "one.txt"); + + txn.commit(); + txn = transactionService.getUserTransaction(false); + txn.begin(); + + TestUtilities.declareRecord(recordOne, unprotectedNodeService, rmActionService); + + // Check the disposition action + assertTrue(this.nodeService.hasAspect(recordOne, ASPECT_DISPOSITION_LIFECYCLE)); + assertFalse(this.nodeService.hasAspect(recordFolder, ASPECT_DISPOSITION_LIFECYCLE)); + + // Check the dispostion action + DispositionAction da = dispositionService.getNextDispositionAction(recordOne); + assertNotNull(da); + assertEquals("cutoff", da.getDispositionActionDefinition().getName()); + assertNull(da.getAsOfDate()); + assertFalse((Boolean)this.nodeService.getProperty(da.getNodeRef(), PROP_DISPOSITION_EVENTS_ELIGIBLE)); + assertEquals(true, da.getDispositionActionDefinition().eligibleOnFirstCompleteEvent()); + List events = da.getEventCompletionDetails(); + assertNotNull(events); + assertEquals(1, events.size()); + EventCompletionDetails event = events.get(0); + + Map params = new HashMap(3); + params.put(CompleteEventAction.PARAM_EVENT_NAME, event.getEventName()); + params.put(CompleteEventAction.PARAM_EVENT_COMPLETED_AT, new Date()); + params.put(CompleteEventAction.PARAM_EVENT_COMPLETED_BY, "roy"); + + this.rmActionService.executeRecordsManagementAction(recordOne, "completeEvent", params); + + txn.commit(); + txn = transactionService.getUserTransaction(false); + txn.begin(); + + assertTrue((Boolean)this.nodeService.getProperty(da.getNodeRef(), PROP_DISPOSITION_EVENTS_ELIGIBLE)); + + // Do the commit action + this.rmActionService.executeRecordsManagementAction(recordOne, "cutoff", null); + + txn.commit(); + txn = transactionService.getUserTransaction(false); + txn.begin(); + + // Check events are gone + da = dispositionService.getNextDispositionAction(recordOne); + + assertNotNull(da); + assertEquals("destroy", da.getDispositionActionDefinition().getName()); + assertNotNull(da.getAsOfDate()); + assertFalse((Boolean)this.nodeService.getProperty(da.getNodeRef(), PROP_DISPOSITION_EVENTS_ELIGIBLE)); + events = da.getEventCompletionDetails(); + assertNotNull(events); + assertEquals(0, events.size()); + + final Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.HOUR, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + + // Clock the asOf date back to ensure eligibility for destruction + NodeRef ndNodeRef = nodeService.getChildAssocs(recordOne, ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + Date nowDate = calendar.getTime(); + assertFalse(nowDate.equals(nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_AS_OF))); + params.clear(); + params.put(EditDispositionActionAsOfDateAction.PARAM_AS_OF_DATE, nowDate); + rmActionService.executeRecordsManagementAction(recordOne, "editDispositionActionAsOfDate", params); + assertTrue(nowDate.equals(nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_AS_OF))); + + + assertNotNull(nodeService.getProperty(recordOne, ContentModel.PROP_CONTENT)); + + rmActionService.executeRecordsManagementAction(recordOne, "destroy", null); + + // Check that the node has been ghosted + assertTrue(nodeService.exists(recordOne)); + assertTrue(nodeService.hasAspect(recordOne, RecordsManagementModel.ASPECT_GHOSTED)); + assertNull(nodeService.getProperty(recordOne, ContentModel.PROP_CONTENT)); + + txn.commit(); + } + + public void testDispositionLifecycle_0412_03_eventtest() throws Exception + { + NodeRef recordCategory = TestUtilities.getRecordCategory(this.searchService, "Military Files", "Personnel Security Program Records"); + assertNotNull(recordCategory); + assertEquals("Personnel Security Program Records", this.nodeService.getProperty(recordCategory, ContentModel.PROP_NAME)); + + Map folderProps = new HashMap(1); + folderProps.put(ContentModel.PROP_NAME, "My Folder"); + NodeRef recordFolder = this.nodeService.createNode(recordCategory, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "My Folder"), + TYPE_RECORD_FOLDER).getChildRef(); + setComplete(); + endTransaction(); + + UserTransaction txn = transactionService.getUserTransaction(false); + txn.begin(); + + NodeRef recordOne = createRecord(recordFolder); + + txn.commit(); + txn = transactionService.getUserTransaction(false); + txn.begin(); + + TestUtilities.declareRecord(recordOne, unprotectedNodeService, rmActionService); + + // NOTE the disposition is being managed at a folder level ... + + // Check the disposition action + assertFalse(this.nodeService.hasAspect(recordOne, ASPECT_DISPOSITION_LIFECYCLE)); + assertTrue(this.nodeService.hasAspect(recordFolder, ASPECT_DISPOSITION_LIFECYCLE)); + + // Check the dispostion action + DispositionAction da = dispositionService.getNextDispositionAction(recordFolder); + assertNotNull(da); + assertEquals("cutoff", da.getDispositionActionDefinition().getName()); + assertNull(da.getAsOfDate()); + assertFalse((Boolean)this.nodeService.getProperty(da.getNodeRef(), PROP_DISPOSITION_EVENTS_ELIGIBLE)); + assertEquals(false, da.getDispositionActionDefinition().eligibleOnFirstCompleteEvent()); + List events = da.getEventCompletionDetails(); + assertNotNull(events); + assertEquals(3, events.size()); + + checkSearchAspect(recordFolder); + + txn.commit(); + txn = transactionService.getUserTransaction(false); + txn.begin(); + + EventCompletionDetails ecd = events.get(0); + assertFalse(ecd.isEventComplete()); + assertNull(ecd.getEventCompletedBy()); + assertNull(ecd.getEventCompletedAt()); + + Map params = new HashMap(3); + params.put(CompleteEventAction.PARAM_EVENT_NAME, events.get(0).getEventName()); + params.put(CompleteEventAction.PARAM_EVENT_COMPLETED_AT, new Date()); + params.put(CompleteEventAction.PARAM_EVENT_COMPLETED_BY, "roy"); + + checkSearchAspect(recordFolder); + + this.rmActionService.executeRecordsManagementAction(recordFolder, "completeEvent", params); + + txn.commit(); + txn = transactionService.getUserTransaction(false); + txn.begin(); + + assertFalse((Boolean)this.nodeService.getProperty(da.getNodeRef(), PROP_DISPOSITION_EVENTS_ELIGIBLE)); + assertEquals(false, da.getDispositionActionDefinition().eligibleOnFirstCompleteEvent()); + events = da.getEventCompletionDetails(); + assertNotNull(events); + assertEquals(3, events.size()); + + params = new HashMap(3); + params.put(CompleteEventAction.PARAM_EVENT_NAME, events.get(1).getEventName()); + params.put(CompleteEventAction.PARAM_EVENT_COMPLETED_AT, new Date()); + params.put(CompleteEventAction.PARAM_EVENT_COMPLETED_BY, "roy"); + + checkSearchAspect(recordFolder); + + this.rmActionService.executeRecordsManagementAction(recordFolder, "completeEvent", params); + + txn.commit(); + txn = transactionService.getUserTransaction(false); + txn.begin(); + + assertFalse((Boolean)this.nodeService.getProperty(da.getNodeRef(), PROP_DISPOSITION_EVENTS_ELIGIBLE)); + + params = new HashMap(3); + params.put(CompleteEventAction.PARAM_EVENT_NAME, events.get(2).getEventName()); + params.put(CompleteEventAction.PARAM_EVENT_COMPLETED_AT, new Date()); + params.put(CompleteEventAction.PARAM_EVENT_COMPLETED_BY, "roy"); + + checkSearchAspect(recordFolder); + + this.rmActionService.executeRecordsManagementAction(recordFolder, "completeEvent", params); + + txn.commit(); + txn = transactionService.getUserTransaction(false); + txn.begin(); + + assertTrue((Boolean)this.nodeService.getProperty(da.getNodeRef(), PROP_DISPOSITION_EVENTS_ELIGIBLE)); + + events = da.getEventCompletionDetails(); + assertNotNull(events); + assertEquals(3, events.size()); + for (EventCompletionDetails e : events) + { + assertTrue(e.isEventComplete()); + assertEquals("roy", e.getEventCompletedBy()); + assertNotNull(e.getEventCompletedAt()); + } + + checkSearchAspect(recordFolder); + + // Test undo + + params = new HashMap(1); + params.put(CompleteEventAction.PARAM_EVENT_NAME, events.get(2).getEventName()); + this.rmActionService.executeRecordsManagementAction(recordFolder, "undoEvent", params); + + txn.commit(); + txn = transactionService.getUserTransaction(false); + txn.begin(); + + assertFalse((Boolean)this.nodeService.getProperty(da.getNodeRef(), PROP_DISPOSITION_EVENTS_ELIGIBLE)); + + params = new HashMap(3); + params.put(CompleteEventAction.PARAM_EVENT_NAME, events.get(2).getEventName()); + params.put(CompleteEventAction.PARAM_EVENT_COMPLETED_AT, new Date()); + params.put(CompleteEventAction.PARAM_EVENT_COMPLETED_BY, "roy"); + + this.rmActionService.executeRecordsManagementAction(recordFolder, "completeEvent", params); + + txn.commit(); + txn = transactionService.getUserTransaction(false); + txn.begin(); + + assertTrue((Boolean)this.nodeService.getProperty(da.getNodeRef(), PROP_DISPOSITION_EVENTS_ELIGIBLE)); + + // Do the commit action + this.rmActionService.executeRecordsManagementAction(recordFolder, "cutoff", null); + + txn.commit(); + txn = transactionService.getUserTransaction(false); + txn.begin(); + + // Check events are gone + da = dispositionService.getNextDispositionAction(recordFolder); + + assertNotNull(da); + assertEquals("destroy", da.getDispositionActionDefinition().getName()); + assertNotNull(da.getAsOfDate()); + assertFalse((Boolean)this.nodeService.getProperty(da.getNodeRef(), PROP_DISPOSITION_EVENTS_ELIGIBLE)); + events = da.getEventCompletionDetails(); + assertNotNull(events); + assertEquals(0, events.size()); + + checkSearchAspect(recordFolder); + + txn.commit(); + } + + private NodeRef createRecord(NodeRef recordFolder) + { + return createRecord(recordFolder, "MyRecord.txt"); + } + + private NodeRef createRecord(NodeRef recordFolder, String name) + { + return createRecord(recordFolder, name, "There is some content in this record"); + } + + private NodeRef createRecord(NodeRef recordFolder, String name, String someTextContent) + { + // Create the document + Map props = new HashMap(1); + props.put(ContentModel.PROP_NAME, name); + NodeRef recordOne = this.nodeService.createNode(recordFolder, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, name), + ContentModel.TYPE_CONTENT, + props).getChildRef(); + + // Set the content + ContentWriter writer = this.contentService.getWriter(recordOne, ContentModel.PROP_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.setEncoding("UTF-8"); + writer.putContent(someTextContent); + + return recordOne; + } + + /** + * This method tests the filing of a custom type, as defined in DOD 5015. + */ + public void testFileDOD5015CustomTypes() throws Exception + { + NodeRef recordCategory = TestUtilities.getRecordCategory(this.searchService, "Reports", "AIS Audit Records"); + + NodeRef recordFolder = createRecordFolder(recordCategory, "March AIS Audit Records"); + setComplete(); + endTransaction(); + + UserTransaction txn = transactionService.getUserTransaction(false); + txn.begin(); + + NodeRef testDocument = this.nodeService.createNode(recordFolder, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "CustomType"), + ContentModel.TYPE_CONTENT).getChildRef(); + + // It's not necessary to set content for this test. + + // File the record. + rmActionService.executeRecordsManagementAction(testDocument, "file"); + + assertTrue("testDocument should be a record.", rmService.isRecord(testDocument)); + + // Have the customType aspect applied.. + Map props = new HashMap(); + props.put(PROP_SCANNED_FORMAT.toPrefixString(serviceRegistry.getNamespaceService()), "f"); + props.put(PROP_SCANNED_FORMAT_VERSION.toPrefixString(serviceRegistry.getNamespaceService()), "1.0"); + props.put(PROP_RESOLUTION_X.toPrefixString(serviceRegistry.getNamespaceService()), "100"); + props.put(PROP_RESOLUTION_Y.toPrefixString(serviceRegistry.getNamespaceService()), "100"); + props.put(PROP_SCANNED_BIT_DEPTH.toPrefixString(serviceRegistry.getNamespaceService()), "10"); + rmActionService.executeRecordsManagementAction(testDocument, "applyScannedRecord", props); + + assertTrue("Custom type should have ScannedRecord aspect.", nodeService.hasAspect(testDocument, DOD5015Model.ASPECT_SCANNED_RECORD)); + + txn.rollback(); + } + + public void testFileDOD5015CustomTypes2() throws Exception + { + NodeRef recordCategory = TestUtilities.getRecordCategory(this.searchService, "Reports", "AIS Audit Records"); + + NodeRef recordFolder = createRecordFolder(recordCategory, "March AIS Audit Records"); + setComplete(); + endTransaction(); + + UserTransaction txn = transactionService.getUserTransaction(false); + txn.begin(); + + NodeRef testDocument = this.nodeService.createNode(recordFolder, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "CustomType"), + ContentModel.TYPE_CONTENT).getChildRef(); + + // It's not necessary to set content for this test. + + // File the record + List aspects = new ArrayList(1); + aspects.add(DOD5015Model.ASPECT_SCANNED_RECORD); + Map props = new HashMap(1); + props.put(FileAction.PARAM_RECORD_METADATA_ASPECTS, (Serializable)aspects); + rmActionService.executeRecordsManagementAction(testDocument, "file", props); + + assertTrue("testDocument should be a record.", rmService.isRecord(testDocument)); + assertTrue("Custom type should have ScannedRecord aspect.", nodeService.hasAspect(testDocument, DOD5015Model.ASPECT_SCANNED_RECORD)); + + txn.rollback(); + } + + /** + * This method tests the filing of an already existing document i.e. one that is + * already contained within the document library. + */ + public void testFileFromDoclib() throws Exception + { + // Get the relevant RecordCategory and create a RecordFolder underneath it. + NodeRef recordCategory = TestUtilities.getRecordCategory(this.searchService, "Reports", "AIS Audit Records"); + + NodeRef recordFolder = createRecordFolder(recordCategory, "March AIS Audit Records"); + setComplete(); + endTransaction(); + + UserTransaction txn = transactionService.getUserTransaction(false); + txn.begin(); + + // Unlike testBasicFilingTest, we now create a normal Alfresco content node + // rather than a fully-fledged record. The content must also be outside the + // fileplan. + + // Create a site - to put the content in. + final String rmTestSiteShortName = "rmTest" + System.currentTimeMillis(); + this.serviceRegistry.getSiteService().createSite("RMTestSite", rmTestSiteShortName, + "Test site for Records Management", "", SiteVisibility.PUBLIC); + + NodeRef siteRoot = this.serviceRegistry.getSiteService().getSite(rmTestSiteShortName).getNodeRef(); + NodeRef siteDocLib = this.nodeService.createNode(siteRoot, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "documentLibrary"), + ContentModel.TYPE_FOLDER).getChildRef(); + // Create the test document + NodeRef testDocument = this.nodeService.createNode(siteDocLib, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "PreexistingDocument.txt"), + ContentModel.TYPE_CONTENT).getChildRef(); + // Set some content + ContentWriter writer = this.contentService.getWriter(testDocument, ContentModel.PROP_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.setEncoding("UTF-8"); + writer.putContent("Some dummy content."); + + txn.commit(); + txn = transactionService.getUserTransaction(false); + txn.begin(); + + // Clearly, this should not be a record at this point. + assertFalse(this.nodeService.hasAspect(testDocument, ASPECT_RECORD)); + + // Now we want to file this document as a record within the RMA. + // To do this we simply move a document into the fileplan and file + this.serviceRegistry.getFileFolderService().move(testDocument, recordFolder, null); + rmActionService.executeRecordsManagementAction(testDocument, "file"); + + assertTrue("testDocument should be a record.", rmService.isRecord(testDocument)); + assertNotNull(this.nodeService.getProperty(testDocument, PROP_IDENTIFIER)); + assertNotNull(this.nodeService.getProperty(testDocument, PROP_DATE_FILED)); + + // Check the review schedule + assertTrue(this.nodeService.hasAspect(testDocument, ASPECT_VITAL_RECORD)); + assertNotNull(this.nodeService.getProperty(testDocument, PROP_REVIEW_AS_OF)); + + txn.commit(); + } + + /** + * This method tests the filing of non-electronic record. + */ + public void testFileNonElectronicRecord() throws Exception + { + setComplete(); + endTransaction(); + + // Create a record folder + final NodeRef recordFolder = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + // Get the relevant RecordCategory and create a RecordFolder underneath it. + NodeRef recordCategory = TestUtilities.getRecordCategory(searchService, "Reports", "AIS Audit Records"); + NodeRef result = createRecordFolder(recordCategory, "March AIS Audit Records" + System.currentTimeMillis()); + + return result; + } + }); + + // Create a non-electronic record + final NodeRef nonElectronicTestRecord = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + // Create the document + NodeRef result = nodeService.createNode(recordFolder, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "Non-electronic Record" + System.currentTimeMillis()), + RecordsManagementModel.TYPE_NON_ELECTRONIC_DOCUMENT).getChildRef(); + + // There is no content on a non-electronic record. + + // These properties are required in order to declare the record. + Map props = nodeService.getProperties(result); + props.put(RecordsManagementModel.PROP_ORIGINATING_ORGANIZATION, "alfresco"); + props.put(RecordsManagementModel.PROP_ORIGINATOR, "admin"); + props.put(RecordsManagementModel.PROP_PUBLICATION_DATE, new Date()); + + Calendar fileCalendar = Calendar.getInstance(); + String year = Integer.toString(fileCalendar.get(Calendar.YEAR)); + props.put(RecordsManagementModel.PROP_DATE_FILED, fileCalendar.getTime()); + + String recordId = year + "-" + nodeService.getProperty(result, ContentModel.PROP_NODE_DBID).toString(); + props.put(RecordsManagementModel.PROP_IDENTIFIER, recordId); + + + nodeService.setProperties(result, props); + + return result; + } + }); + + // File and declare the record + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + assertTrue("Expected non-electronic record to be a record.", rmService.isRecord(nonElectronicTestRecord)); + assertFalse("Expected non-electronic record not to be declared yet.", rmService.isRecordDeclared(nonElectronicTestRecord)); + + rmActionService.executeRecordsManagementAction(nonElectronicTestRecord, "declareRecord"); + + assertTrue("Non-electronic record should now be declared.", rmService.isRecordDeclared(nonElectronicTestRecord)); + + // These properties are added automatically when the record is filed + assertNotNull(nodeService.getProperty(nonElectronicTestRecord, RecordsManagementModel.PROP_IDENTIFIER)); + assertNotNull(nodeService.getProperty(nonElectronicTestRecord, RecordsManagementModel.PROP_DATE_FILED)); + +// assertNotNull(nodeService.getProperty(testRecord, ContentModel.PROP_TITLE)); +// assertNotNull(nodeService.getProperty(testRecord, RecordsManagementModel.PROP_SUPPLEMENTAL_MARKING_LIST)); +// assertNotNull(nodeService.getProperty(testRecord, RecordsManagementModel.PROP_MEDIA_TYPE)); +// assertNotNull(nodeService.getProperty(testRecord, RecordsManagementModel.PROP_FORMAT)); +// assertNotNull(nodeService.getProperty(testRecord, RecordsManagementModel.PROP_DATE_RECEIVED)); +// assertEquals("foo", nodeService.getProperty(testRecord, RecordsManagementModel.PROP_ADDRESS)); +// assertEquals("foo", nodeService.getProperty(testRecord, RecordsManagementModel.PROP_OTHER_ADDRESS)); +// assertNotNull(nodeService.getProperty(testRecord, RecordsManagementModel.PROP_LOCATION)); +// assertEquals("foo", nodeService.getProperty(testRecord, RecordsManagementModel.PROP_PROJECT_NAME)); + + //TODO Add links to other records as per test doc. + return null; + } + }); + } + + private NodeRef createRecordFolder(NodeRef recordCategory, String folderName) + { + Map folderProps = new HashMap(1); + folderProps.put(ContentModel.PROP_NAME, folderName); + NodeRef recordFolder = this.nodeService.createNode(recordCategory, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, folderName), + TYPE_RECORD_FOLDER).getChildRef(); + return recordFolder; + } + + /** + * Caveat Config + * + * @throws Exception + */ + public void testCaveatConfig() throws Exception + { + setComplete(); + endTransaction(); + + cleanCaveatConfigData(); + setupCaveatConfigData(); + + // set/reset allowed values (empty list by default) + + final List newValues = new ArrayList(4); + newValues.add(NOFORN); + newValues.add(NOCONTRACT); + newValues.add(FOUO); + newValues.add(FGI); + + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + rmAdminService.changeCustomConstraintValues(RecordsManagementCustomModel.CONSTRAINT_CUSTOM_SMLIST, newValues); + + return null; + } + }); + + final NodeRef recordFolder = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + // Test list of allowed values for caveats + + List allowedValues = AuthenticationUtil.runAs(new RunAsWork>() + { + public List doWork() + { + // get allowed values for given caveat (for current user) + return caveatConfigService.getRMAllowedValues("rmc:smList"); + } + }, "dfranco"); + + assertEquals(2, allowedValues.size()); + assertTrue(allowedValues.contains(NOFORN)); + assertTrue(allowedValues.contains(FOUO)); + + + allowedValues = AuthenticationUtil.runAs(new RunAsWork>() + { + public List doWork() + { + // get allowed values for given caveat (for current user) + return caveatConfigService.getRMAllowedValues("rmc:smList"); + } + }, "dmartinz"); + + assertEquals(4, allowedValues.size()); + assertTrue(allowedValues.contains(NOFORN)); + assertTrue(allowedValues.contains(NOCONTRACT)); + assertTrue(allowedValues.contains(FOUO)); + assertTrue(allowedValues.contains(FGI)); + + + // Create record category / record folder + + final NodeRef recordCategory = TestUtilities.getRecordCategory(searchService, "Reports", "AIS Audit Records"); + assertNotNull(recordCategory); + assertEquals("AIS Audit Records", nodeService.getProperty(recordCategory, ContentModel.PROP_NAME)); + + NodeRef recordFolder = createRecordFolder(recordCategory, "March AIS Audit Records"); + assertNotNull(recordFolder); + assertEquals(TYPE_RECORD_FOLDER, nodeService.getType(recordFolder)); + + // set RM capabilities on the file plan - to view & read records + setPermission(filePlan, PermissionService.ALL_AUTHORITIES, RMPermissionModel.VIEW_RECORDS, true); + setPermission(filePlan, PermissionService.ALL_AUTHORITIES, RMPermissionModel.READ_RECORDS, true); + + // set RM capabilities on the record folder - to read records + setPermission(recordFolder, PermissionService.ALL_AUTHORITIES, RMPermissionModel.READ_RECORDS, true); + + + return recordFolder; + } + }); + + final String RECORD_NAME = "MyRecord"+System.currentTimeMillis()+".txt"; + final String SOME_CONTENT = "There is some content in this record"; + + final NodeRef recordOne = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + int expectedChildCount = nodeService.getChildAssocs(recordFolder).size(); + + NodeRef recordOne = createRecord(recordFolder, RECORD_NAME, SOME_CONTENT); + + assertEquals(expectedChildCount+1, nodeService.getChildAssocs(recordFolder).size()); + + return recordOne; + } + }); + + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + assertTrue(nodeService.hasAspect(recordOne, ASPECT_RECORD)); + + int expectedChildCount = nodeService.getChildAssocs(recordFolder).size()-1; + + // + // Test caveats (security interceptors) BEFORE setting properties + // + + sanityCheckAccess("dmartinz", recordFolder, recordOne, RECORD_NAME, SOME_CONTENT, true, expectedChildCount); + sanityCheckAccess("gsmith", recordFolder, recordOne, RECORD_NAME, SOME_CONTENT, true, expectedChildCount); + sanityCheckAccess("dsandy", recordFolder, recordOne, RECORD_NAME, SOME_CONTENT, true, expectedChildCount); + + // Test setting properties (with restricted set of allowed values) + + // Set supplemental markings list (on record) + // TODO - set supplemental markings list (on record folder) + + AuthenticationUtil.runAs(new RunAsWork() + { + public Object doWork() + { + // set RM capabilities on the file plan - to file records and add/edit properties (ie. edit record) + setPermission(filePlan, "dfranco", RMPermissionModel.FILING, true); + setPermission(filePlan, "dfranco", RMPermissionModel.EDIT_RECORD_METADATA, true); + return null; + } + }, "admin"); + + + AuthenticationUtil.setFullyAuthenticatedUser("dfranco"); + assertEquals(AccessStatus.ALLOWED, publicServiceAccessService.hasAccess("NodeService", "exists", recordFolder)); + + return null; + } + }); + + try + { + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + + // Set smList + + Map propValues = new HashMap(1); + List smList = new ArrayList(3); + smList.add(FOUO); + smList.add(NOFORN); + smList.add(NOCONTRACT); + propValues.put(RecordsManagementModel.PROP_SUPPLEMENTAL_MARKING_LIST, (Serializable)smList); + nodeService.addProperties(recordOne, propValues); + + return null; + } + }); + + fail("Should fail with integrity exception"); // user 'dfranco' not allowed 'NOCONTRACT' + } + catch (IntegrityException ie) + { + // expected + } + + try + { + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + // Set smList + + Map propValues = new HashMap(1); + List smList = new ArrayList(2); + smList.add(FOUO); + smList.add(NOFORN); + propValues.put(RecordsManagementModel.PROP_SUPPLEMENTAL_MARKING_LIST, (Serializable)smList); + nodeService.addProperties(recordOne, propValues); + + return null; + } + }); + } + catch (IntegrityException ie) + { + fail(""+ie); + } + + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + @SuppressWarnings("unchecked") + List smList = (List)nodeService.getProperty(recordOne, RecordsManagementModel.PROP_SUPPLEMENTAL_MARKING_LIST); + assertEquals(2, smList.size()); + assertTrue(smList.contains(NOFORN)); + assertTrue(smList.contains(FOUO)); + + return null; + } + }); + + // User-defined field (in this case, "rmc:prjList" on record) + + // Create custom constraint (or reset values if it already exists) + + // create new custom constraint + + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + try + { + List emptyList = new ArrayList(0); + rmAdminService.addCustomConstraintDefinition(CONSTRAINT_CUSTOM_PRJLIST, "Some Projects", true, emptyList, MatchLogic.AND); + } + catch (AlfrescoRuntimeException e) + { + // ignore - ie. assume exception is due to the fact that it already exists + } + + return null; + } + }); + + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + List newerValues = new ArrayList(3); + newerValues.add(PRJ_A); + newerValues.add(PRJ_B); + newerValues.add(PRJ_C); + + rmAdminService.changeCustomConstraintValues(CONSTRAINT_CUSTOM_PRJLIST, newerValues); + + return null; + } + }); + + // define custom property and reference custom constraint + + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + try + { + // Define a custom "project list" property (for records) - note: multi-valued + rmAdminService.addCustomPropertyDefinition( + PROP_CUSTOM_PRJLIST, + ASPECT_RECORD, + PROP_CUSTOM_PRJLIST.getLocalName(), + DataTypeDefinition.TEXT, "Projects", + null, + null, + true, + false, + false, + CONSTRAINT_CUSTOM_PRJLIST); + } + catch (AlfrescoRuntimeException e) + { + // ignore - ie. assume exception is due to the fact that it already exists + } + + return null; + } + }); + + try + { + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + + // Set prjList + + Map propValues = new HashMap(1); + List prjList = new ArrayList(3); + prjList.add(PRJ_A); + prjList.add(PRJ_B); + propValues.put(PROP_CUSTOM_PRJLIST, (Serializable)prjList); + nodeService.addProperties(recordOne, propValues); + + return null; + } + }); + + fail("Should fail with integrity exception"); // user 'dfranco' not allowed 'Project B' + } + catch (IntegrityException ie) + { + // expected + } + + try + { + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + // Set prjList + Map propValues = new HashMap(1); + List prjList = new ArrayList(3); + prjList.add(PRJ_A); + propValues.put(PROP_CUSTOM_PRJLIST, (Serializable)prjList); + nodeService.addProperties(recordOne, propValues); + + return null; + } + }); + } + catch (IntegrityException ie) + { + fail(""+ie); + } + + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + @SuppressWarnings("unchecked") + List prjList = (List)nodeService.getProperty(recordOne, PROP_CUSTOM_PRJLIST); + assertEquals(1, prjList.size()); + assertTrue(prjList.contains(PRJ_A)); + + return null; + } + }); + + // + // Test caveats (security interceptors) AFTER setting properties + // + + int expectedChildCount = nodeService.getChildAssocs(recordFolder).size()-1; + sanityCheckAccess("dmartinz", recordFolder, recordOne, RECORD_NAME, SOME_CONTENT, true, expectedChildCount); + sanityCheckAccess("gsmith", recordFolder, recordOne, RECORD_NAME, SOME_CONTENT, false, expectedChildCount); // denied by rma:prjList ("Project A") + + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + addToGroup("gsmith", "Engineering"); + + return null; + } + }); + + sanityCheckAccess("gsmith", recordFolder, recordOne, RECORD_NAME, SOME_CONTENT, true, expectedChildCount); + sanityCheckAccess("dsandy", recordFolder, recordOne, RECORD_NAME, SOME_CONTENT, false, expectedChildCount); // denied by rma:smList ("NOFORN", "FOUO") + + cleanCaveatConfigData(); + } + + private void setPermission(NodeRef nodeRef, String authority, String permission, boolean allow) + { + permissionService.setPermission(nodeRef, authority, permission, allow); + if (permission.equals(RMPermissionModel.FILING)) + { + if (rmService.isRecordCategory(nodeRef) == true) + { + List assocs = nodeService.getChildAssocs(nodeRef, ContentModel.ASSOC_CONTAINS, RegexQNamePattern.MATCH_ALL); + for (ChildAssociationRef assoc : assocs) + { + NodeRef child = assoc.getChildRef(); + if (rmService.isRecordFolder(child) == true || rmService.isRecordCategory(child) == true) + { + setPermission(child, authority, permission, allow); + } + } + } + } + } + + private void cleanCaveatConfigData() + { + startNewTransaction(); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + + deleteUser("jrangel"); + deleteUser("dmartinz"); + deleteUser("jrogers"); + deleteUser("hmcneil"); + deleteUser("dfranco"); + deleteUser("gsmith"); + deleteUser("eharris"); + deleteUser("bbayless"); + deleteUser("mhouse"); + deleteUser("aly"); + deleteUser("dsandy"); + deleteUser("driggs"); + deleteUser("test1"); + + deleteGroup("Engineering"); + deleteGroup("Finance"); + deleteGroup("test1"); + + caveatConfigService.updateOrCreateCaveatConfig("{}"); // empty config ! + + setComplete(); + endTransaction(); + } + + private void setupCaveatConfigData() + { + startNewTransaction(); + + // Switch to admin + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + + // Create test users/groups (if they do not already exist) + + createUser("jrangel"); + createUser("dmartinz"); + createUser("jrogers"); + createUser("hmcneil"); + createUser("dfranco"); + createUser("gsmith"); + createUser("eharris"); + createUser("bbayless"); + createUser("mhouse"); + createUser("aly"); + createUser("dsandy"); + createUser("driggs"); + createUser("test1"); + + createGroup("Engineering"); + createGroup("Finance"); + createGroup("test1"); + + addToGroup("jrogers", "Engineering"); + addToGroup("dfranco", "Finance"); + + // not in grouo to start with - added later + //addToGroup("gsmith", "Engineering"); + + File file = new File(System.getProperty("user.dir")+"/test-resources/testCaveatConfig2.json"); // from test-resources + assertTrue(file.exists()); + + caveatConfigService.updateOrCreateCaveatConfig(file); + + setComplete(); + endTransaction(); + } + + protected void createUser(String userName) + { + if (! authenticationService.authenticationExists(userName)) + { + authenticationService.createAuthentication(userName, "PWD".toCharArray()); + } + + if (! personService.personExists(userName)) + { + PropertyMap ppOne = new PropertyMap(4); + ppOne.put(ContentModel.PROP_USERNAME, userName); + ppOne.put(ContentModel.PROP_FIRSTNAME, "firstName"); + ppOne.put(ContentModel.PROP_LASTNAME, "lastName"); + ppOne.put(ContentModel.PROP_EMAIL, "email@email.com"); + ppOne.put(ContentModel.PROP_JOBTITLE, "jobTitle"); + + personService.createPerson(ppOne); + } + } + + protected void deleteUser(String userName) + { + if (personService.personExists(userName)) + { + personService.deletePerson(userName); + } + } + + protected void createGroup(String groupShortName) + { + createGroup(null, groupShortName); + } + + protected void createGroup(String parentGroupShortName, String groupShortName) + { + if (parentGroupShortName != null) + { + String parentGroupFullName = authorityService.getName(AuthorityType.GROUP, parentGroupShortName); + if (authorityService.authorityExists(parentGroupFullName) == false) + { + authorityService.createAuthority(AuthorityType.GROUP, groupShortName, groupShortName, null); + authorityService.addAuthority(parentGroupFullName, groupShortName); + } + } + else + { + authorityService.createAuthority(AuthorityType.GROUP, groupShortName, groupShortName, null); + } + } + + protected void deleteGroup(String groupShortName) + { + String groupFullName = authorityService.getName(AuthorityType.GROUP, groupShortName); + if (authorityService.authorityExists(groupFullName) == true) + { + authorityService.deleteAuthority(groupFullName); + } + } + + protected void addToGroup(String authorityName, String groupShortName) + { + authorityService.addAuthority(authorityService.getName(AuthorityType.GROUP, groupShortName), authorityName); + } + + protected void removeFromGroup(String authorityName, String groupShortName) + { + authorityService.removeAuthority(authorityService.getName(AuthorityType.GROUP, groupShortName), authorityName); + } + + private void sanityCheckAccess(String user, NodeRef recordFolder, NodeRef record, String expectedName, String expectedContent, boolean expectedAllowed, int baseCount) + { + //startNewTransaction(); + + AuthenticationUtil.setFullyAuthenticatedUser(user); + + // Sanity check search service - eg. query + + String query = "ID:"+AbstractLuceneQueryParser.escape(record.toString()); + ResultSet rs = this.searchService.query(SPACES_STORE, SearchService.LANGUAGE_LUCENE, query); + + if (expectedAllowed) + { + assertEquals(1, rs.length()); + assertEquals(record.toString(), rs.getNodeRef(0).toString()); + } + else + { + assertEquals(0, rs.length()); + } + rs.close(); + + // Sanity check node service - eg. getProperty, getChildAssocs + + try + { + Serializable value = this.nodeService.getProperty(record, ContentModel.PROP_NAME); + + if (expectedAllowed) + { + assertNotNull(value); + assertEquals(expectedName, (String)value); + } + else + { + fail("Unexpected - access should be denied by caveats"); + } + } + catch (AccessDeniedException ade) + { + if (expectedAllowed) + { + fail("Unexpected - access should be allowed by caveats"); + } + + // expected + } + + List childAssocs = nodeService.getChildAssocs(recordFolder); + + if (expectedAllowed) + { + assertEquals(baseCount+1, childAssocs.size()); + assertEquals(record.toString(), childAssocs.get(baseCount).getChildRef().toString()); + } + else + { + assertEquals(baseCount, childAssocs.size()); + } + + // Sanity check content service - eg. getReader + + try + { + ContentReader reader = this.contentService.getReader(record, ContentModel.PROP_CONTENT); + + if (expectedAllowed) + { + assertNotNull(reader); + assertEquals(expectedContent, reader.getContentString()); + } + else + { + fail("Unexpected - access should be denied by caveats"); + } + } + catch (AccessDeniedException ade) + { + if (expectedAllowed) + { + fail("Unexpected - access should be allowed by caveats"); + } + + // expected + } + + //setComplete(); + //endTransaction(); + } + + /** + * https://issues.alfresco.com/jira/browse/ETHREEOH-3587 + */ + public void testETHREEOH3587() + { + NodeRef recordFolder = TestUtilities.getRecordFolder(searchService, "Reports", "AIS Audit Records", "January AIS Audit Records"); + assertNotNull(recordFolder); + + // Create a record + final NodeRef record = createRecord(recordFolder, GUID.generate()); + + // Commit in order to trigger the setUpRecordFolder behaviour + setComplete(); + endTransaction(); + + // Now try and update the id, this should fail + try + { + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + // Lets just check the record identifier has been set + String id = (String)nodeService.getProperty(record, RecordsManagementModel.PROP_IDENTIFIER); + assertNotNull(id); + + nodeService.setProperty(record, RecordsManagementModel.PROP_IDENTIFIER, "randomValue"); + + return null; + } + }); + + fail("You should not be allowed to update the identifier of a record once it has been created."); + } + catch(AlfrescoRuntimeException e) + { + // Expected + } + + // TODO set the identifier of the second record to be the same as the first .... + } + + /** + * Vital Record Test + * + * @throws Exception + */ + public void testVitalRecords() throws Exception + { + // + // Create a record folder under a "vital" category + // + + // TODO Don't think I need to do this. Can I reuse the existing January one? + + NodeRef vitalRecCategory = + TestUtilities.getRecordCategory(this.searchService, "Reports", "AIS Audit Records"); + + assertNotNull(vitalRecCategory); + assertEquals("AIS Audit Records", + this.nodeService.getProperty(vitalRecCategory, ContentModel.PROP_NAME)); + + NodeRef vitalRecFolder = this.nodeService.createNode(vitalRecCategory, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, + "March AIS Audit Records"), + TYPE_RECORD_FOLDER).getChildRef(); + setComplete(); + endTransaction(); + UserTransaction txn1 = transactionService.getUserTransaction(false); + txn1.begin(); + + // Check the Vital Record data + VitalRecordDefinition vitalRecCatDefinition = vitalRecordService.getVitalRecordDefinition(vitalRecCategory); + assertNotNull("This record category should have a VitalRecordDefinition", vitalRecCatDefinition); + assertTrue(vitalRecCatDefinition.isEnabled()); + + VitalRecordDefinition vitalRecFolderDefinition = vitalRecordService.getVitalRecordDefinition(vitalRecFolder); + assertNotNull("This record folder should have a VitalRecordDefinition", vitalRecFolderDefinition); + assertTrue(vitalRecFolderDefinition.isEnabled()); + + assertEquals("The Vital Record reviewPeriod in the folder did not match its parent category", + vitalRecFolderDefinition.getReviewPeriod(), + vitalRecCatDefinition.getReviewPeriod()); + + // check the search aspect for both the category and folder + checkSearchAspect(vitalRecFolder); + + // Create a vital record + NodeRef vitalRecord = this.nodeService.createNode(vitalRecFolder, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, + "MyVitalRecord" + System.currentTimeMillis() +".txt"), + ContentModel.TYPE_CONTENT).getChildRef(); + + // Set the content + ContentWriter writer = this.contentService.getWriter(vitalRecord, ContentModel.PROP_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.setEncoding("UTF-8"); + writer.putContent("There is some content in this record"); + + rmActionService.executeRecordsManagementAction(vitalRecord, "file"); + + txn1.commit(); + + UserTransaction txn2 = transactionService.getUserTransaction(false); + txn2.begin(); + + // Check the review schedule + + assertTrue(this.nodeService.hasAspect(vitalRecord, ASPECT_VITAL_RECORD)); + VitalRecordDefinition vitalRecDefinition = vitalRecordService.getVitalRecordDefinition(vitalRecord); + assertTrue(vitalRecDefinition.isEnabled()); + Date vitalRecordAsOfDate = (Date)this.nodeService.getProperty(vitalRecord, PROP_REVIEW_AS_OF); + assertNotNull("vitalRecord should have a reviewAsOf date.", vitalRecordAsOfDate); + + // check the search aspect for the vital record + checkSearchAspect(vitalRecord); + + // + // Create a record folder under a "non-vital" category + // + NodeRef nonVitalRecordCategory = TestUtilities.getRecordCategory(this.searchService, "Reports", "Unit Manning Documents"); + assertNotNull(nonVitalRecordCategory); + assertEquals("Unit Manning Documents", this.nodeService.getProperty(nonVitalRecordCategory, ContentModel.PROP_NAME)); + + NodeRef nonVitalFolder = this.nodeService.createNode(nonVitalRecordCategory, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "4th Quarter Unit Manning Documents"), + TYPE_RECORD_FOLDER).getChildRef(); + txn2.commit(); + + UserTransaction txn3 = transactionService.getUserTransaction(false); + txn3.begin(); + + // Check the Vital Record data + assertFalse(vitalRecordService.getVitalRecordDefinition(nonVitalRecordCategory).isEnabled()); + assertFalse(vitalRecordService.getVitalRecordDefinition(nonVitalFolder).isEnabled()); + assertEquals("The Vital Record reviewPeriod in the folder did not match its parent category", + vitalRecordService.getVitalRecordDefinition(nonVitalFolder).getReviewPeriod(), + vitalRecordService.getVitalRecordDefinition(nonVitalRecordCategory).getReviewPeriod()); + + // Create a record + NodeRef nonVitalRecord = this.nodeService.createNode(nonVitalFolder, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "MyNonVitalRecord.txt"), + ContentModel.TYPE_CONTENT).getChildRef(); + + // Set content + writer = this.contentService.getWriter(nonVitalRecord, ContentModel.PROP_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.setEncoding("UTF-8"); + writer.putContent("There is some content in this record"); + + this.rmActionService.executeRecordsManagementAction(nonVitalRecord, "file"); + + txn3.commit(); + + UserTransaction txn4 = transactionService.getUserTransaction(false); + txn4.begin(); + + // Check the review schedule + assertFalse(this.nodeService.hasAspect(nonVitalRecord, ASPECT_VITAL_RECORD)); + assertFalse(vitalRecordService.getVitalRecordDefinition(nonVitalRecord).isEnabled()); + assertEquals("The Vital Record reviewPeriod did not match its parent category", + vitalRecordService.getVitalRecordDefinition(nonVitalRecord).getReviewPeriod(), + vitalRecordService.getVitalRecordDefinition(nonVitalFolder).getReviewPeriod()); + + // Declare as a record + assertTrue(this.nodeService.hasAspect(nonVitalRecord, ASPECT_RECORD)); + + assertTrue("Declared record already on prior to test", + this.nodeService.hasAspect(nonVitalRecord, ASPECT_DECLARED_RECORD) == false); + + + this.nodeService.setProperty(nonVitalRecord, RecordsManagementModel.PROP_PUBLICATION_DATE, new Date()); + List smList = new ArrayList(2); + smList.add(FOUO); + smList.add(NOFORN); + this.nodeService.setProperty(nonVitalRecord, RecordsManagementModel.PROP_SUPPLEMENTAL_MARKING_LIST, (Serializable)smList); + this.nodeService.setProperty(nonVitalRecord, RecordsManagementModel.PROP_MEDIA_TYPE, "mediaTypeValue"); + this.nodeService.setProperty(nonVitalRecord, RecordsManagementModel.PROP_FORMAT, "formatValue"); + this.nodeService.setProperty(nonVitalRecord, RecordsManagementModel.PROP_DATE_RECEIVED, new Date()); + this.nodeService.setProperty(nonVitalRecord, RecordsManagementModel.PROP_ORIGINATOR, "origValue"); + this.nodeService.setProperty(nonVitalRecord, RecordsManagementModel.PROP_ORIGINATING_ORGANIZATION, "origOrgValue"); + this.nodeService.setProperty(nonVitalRecord, ContentModel.PROP_TITLE, "titleValue"); + + this.rmActionService.executeRecordsManagementAction(nonVitalRecord, "declareRecord"); + assertTrue(this.nodeService.hasAspect(nonVitalRecord, ASPECT_RECORD)); + assertTrue("Declared aspect not set", this.nodeService.hasAspect(nonVitalRecord, ASPECT_DECLARED_RECORD)); + + // + // Now we will change the vital record indicator in the containers above these records + // and ensure that the change is reflected down to the record. + // + + // 1. Switch parent folder from non-vital to vital. + this.nodeService.setProperty(nonVitalFolder, PROP_VITAL_RECORD_INDICATOR, true); + this.nodeService.setProperty(nonVitalFolder, PROP_REVIEW_PERIOD, "week|1"); + + txn4.commit(); + + UserTransaction txn5 = transactionService.getUserTransaction(false); + txn5.begin(); + + // check the folder search aspect + checkSearchAspect(nonVitalFolder); + + NodeRef formerlyNonVitalRecord = nonVitalRecord; + + assertTrue("Expected VitalRecord aspect not present", nodeService.hasAspect(formerlyNonVitalRecord, ASPECT_VITAL_RECORD)); + VitalRecordDefinition formerlyNonVitalRecordDefinition = vitalRecordService.getVitalRecordDefinition(formerlyNonVitalRecord); + assertNotNull(formerlyNonVitalRecordDefinition); + + assertEquals("The Vital Record reviewPeriod is wrong.", new Period("week|1"), + vitalRecordService.getVitalRecordDefinition(formerlyNonVitalRecord).getReviewPeriod()); + assertNotNull("formerlyNonVitalRecord should now have a reviewAsOf date.", + nodeService.getProperty(formerlyNonVitalRecord, PROP_REVIEW_AS_OF)); + + // check search aspect for the new vital record + checkSearchAspect(formerlyNonVitalRecord); + + // 2. Switch parent folder from vital to non-vital. + this.nodeService.setProperty(vitalRecFolder, PROP_VITAL_RECORD_INDICATOR, false); + + txn5.commit(); + + UserTransaction txn6 = transactionService.getUserTransaction(false); + txn6.begin(); + + NodeRef formerlyVitalRecord = vitalRecord; + + assertTrue("Unexpected VitalRecord aspect present", + nodeService.hasAspect(formerlyVitalRecord, ASPECT_VITAL_RECORD) == false); + VitalRecordDefinition formerlyVitalRecordDefinition = vitalRecordService.getVitalRecordDefinition(formerlyVitalRecord); + assertNotNull(formerlyVitalRecordDefinition); + assertNull("formerlyVitalRecord should now not have a reviewAsOf date.", + nodeService.getProperty(formerlyVitalRecord, PROP_REVIEW_AS_OF)); + + // 3. override the VitalRecordDefinition between Category, Folder, Record and ensure + // the overrides work + + // First switch the non-vital record folder back to vital. + this.nodeService.setProperty(vitalRecFolder, PROP_VITAL_RECORD_INDICATOR, true); + + txn6.commit(); + UserTransaction txn7 = transactionService.getUserTransaction(false); + txn7.begin(); + + assertTrue("Unexpected VitalRecord aspect present", + nodeService.hasAspect(vitalRecord, ASPECT_VITAL_RECORD)); + + // The reviewAsOf date should be changing as the parent review periods are updated. + Date initialReviewAsOfDate = (Date)nodeService.getProperty(vitalRecord, PROP_REVIEW_AS_OF); + assertNotNull("record should have a reviewAsOf date.", + initialReviewAsOfDate); + + // Change some of the VitalRecordDefinition in Record Category + Map recCatProps = this.nodeService.getProperties(vitalRecCategory); + + // Run this test twice (after a clean db) and it fails at the below line. + assertEquals(new Period("week|1"), recCatProps.get(PROP_REVIEW_PERIOD)); + this.nodeService.setProperty(vitalRecCategory, PROP_REVIEW_PERIOD, new Period("day|1")); + + txn7.commit(); + UserTransaction txn8 = transactionService.getUserTransaction(false); + txn8.begin(); + + assertEquals(new Period("day|1"), vitalRecordService.getVitalRecordDefinition(vitalRecCategory).getReviewPeriod()); + assertEquals(new Period("day|1"), vitalRecordService.getVitalRecordDefinition(vitalRecFolder).getReviewPeriod()); + + // check the search aspect of the folder after period change + checkSearchAspect(vitalRecFolder); + + // Change some of the VitalRecordDefinition in Record Folder + Map folderProps = this.nodeService.getProperties(vitalRecFolder); + assertEquals(new Period("day|1"), folderProps.get(PROP_REVIEW_PERIOD)); + this.nodeService.setProperty(vitalRecFolder, PROP_REVIEW_PERIOD, new Period("month|1")); + + txn8.commit(); + UserTransaction txn9 = transactionService.getUserTransaction(false); + txn9.begin(); + + assertEquals(new Period("day|1"), vitalRecordService.getVitalRecordDefinition(vitalRecCategory).getReviewPeriod()); + assertEquals(new Period("month|1"), vitalRecordService.getVitalRecordDefinition(vitalRecFolder).getReviewPeriod()); + + // check the search aspect of the folder after period change + checkSearchAspect(vitalRecFolder); + + // Need to commit the transaction to trigger the behaviour that handles changes to VitalRecord Definition. + txn9.commit(); + UserTransaction txn10 = transactionService.getUserTransaction(false); + txn10.begin(); + + Date newReviewAsOfDate = (Date)nodeService.getProperty(vitalRecord, PROP_REVIEW_AS_OF); + assertNotNull("record should have a reviewAsOf date.", initialReviewAsOfDate); + assertTrue("reviewAsOfDate should have changed.", + initialReviewAsOfDate.toString().equals(newReviewAsOfDate.toString()) == false); + + // check the search aspect of the record after period change + checkSearchAspect(vitalRecord); + + // Now clean up after this test. + nodeService.deleteNode(vitalRecord); + nodeService.deleteNode(vitalRecFolder); + nodeService.deleteNode(nonVitalRecord); + nodeService.deleteNode(nonVitalFolder); + nodeService.setProperty(vitalRecCategory, PROP_REVIEW_PERIOD, new Period("week|1")); + + txn10.commit(); + } + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/JScriptTestSuite.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/JScriptTestSuite.java new file mode 100644 index 0000000000..540127ad94 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/JScriptTestSuite.java @@ -0,0 +1,45 @@ +/* + * 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.module.org_alfresco_module_rm.test; + +import junit.framework.Test; +import junit.framework.TestSuite; + +import org.alfresco.module.org_alfresco_module_rm.test.jscript.RMJScriptTest; + + +/** + * RM JScript test suite + * + * @author Roy Wetherall + */ +public class JScriptTestSuite extends TestSuite +{ + /** + * Creates the test suite + * + * @return the test suite + */ + public static Test suite() + { + TestSuite suite = new TestSuite(); + suite.addTestSuite(RMJScriptTest.class); + return suite; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/ServicesTestSuite.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/ServicesTestSuite.java new file mode 100644 index 0000000000..e76876eab6 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/ServicesTestSuite.java @@ -0,0 +1,57 @@ +/* + * 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.module.org_alfresco_module_rm.test; + +import org.alfresco.module.org_alfresco_module_rm.test.service.DispositionServiceImplTest; +import org.alfresco.module.org_alfresco_module_rm.test.service.RecordsManagementAdminServiceImplTest; +import org.alfresco.module.org_alfresco_module_rm.test.service.RecordsManagementSearchServiceImplTest; +import org.alfresco.module.org_alfresco_module_rm.test.service.RecordsManagementServiceImplTest; +import org.alfresco.module.org_alfresco_module_rm.test.service.VitalRecordServiceImplTest; + +import junit.framework.Test; +import junit.framework.TestSuite; + + +/** + * RM test suite + * + * @author Roy Wetherall + */ +public class ServicesTestSuite extends TestSuite +{ + /** + * Creates the test suite + * + * @return the test suite + */ + public static Test suite() + { + TestSuite suite = new TestSuite(); + suite.addTestSuite(RecordsManagementServiceImplTest.class); + suite.addTestSuite(DispositionServiceImplTest.class); + //suite.addTestSuite(RecordsManagementActionServiceImplTest.class); + suite.addTestSuite(RecordsManagementAdminServiceImplTest.class); + //suite.addTestSuite(RecordsManagementAuditServiceImplTest.class); + //suite.addTestSuite(RecordsManagementEventServiceImplTest.class); + //suite.addTestSuite(RecordsManagementSecurityServiceImplTest.class); + suite.addTestSuite(RecordsManagementSearchServiceImplTest.class); + suite.addTestSuite(VitalRecordServiceImplTest.class); + return suite; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/WebScriptTestSuite.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/WebScriptTestSuite.java new file mode 100644 index 0000000000..a4532b09c6 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/WebScriptTestSuite.java @@ -0,0 +1,57 @@ +/* + * 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.module.org_alfresco_module_rm.test; + +import junit.framework.Test; +import junit.framework.TestSuite; + +import org.alfresco.module.org_alfresco_module_rm.test.webscript.BootstraptestDataRestApiTest; +import org.alfresco.module.org_alfresco_module_rm.test.webscript.DispositionRestApiTest; +import org.alfresco.module.org_alfresco_module_rm.test.webscript.EventRestApiTest; +import org.alfresco.module.org_alfresco_module_rm.test.webscript.RMCaveatConfigScriptTest; +import org.alfresco.module.org_alfresco_module_rm.test.webscript.RMConstraintScriptTest; +import org.alfresco.module.org_alfresco_module_rm.test.webscript.RmRestApiTest; +import org.alfresco.module.org_alfresco_module_rm.test.webscript.RoleRestApiTest; + + +/** + * RM WebScript test suite + * + * @author Roy Wetherall + */ +public class WebScriptTestSuite extends TestSuite +{ + /** + * Creates the test suite + * + * @return the test suite + */ + public static Test suite() + { + TestSuite suite = new TestSuite(); + suite.addTestSuite(BootstraptestDataRestApiTest.class); + suite.addTestSuite(DispositionRestApiTest.class); + suite.addTestSuite(EventRestApiTest.class); + suite.addTestSuite(RMCaveatConfigScriptTest.class); + suite.addTestSuite(RMConstraintScriptTest.class); + suite.addTestSuite(RmRestApiTest.class); + suite.addTestSuite(RoleRestApiTest.class); + return suite; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/AddModifyEventDatesCapabilityTest.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/AddModifyEventDatesCapabilityTest.java new file mode 100644 index 0000000000..4d2dba2b8d --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/AddModifyEventDatesCapabilityTest.java @@ -0,0 +1,282 @@ +/* + * 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.module.org_alfresco_module_rm.test.capabilities; + +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.action.impl.CompleteEventAction; +import org.alfresco.module.org_alfresco_module_rm.action.impl.FreezeAction; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.cmr.security.AccessStatus; + +/** + * @author Roy Wetherall + */ +public class AddModifyEventDatesCapabilityTest extends BaseTestCapabilities +{ + /** + * + * @throws Exception + */ + public void testAddModifyEventDatesCapability() throws Exception + { + // Check file plan permissions + checkPermissions( + filePlan, + ADD_MODIFY_EVENT_DATES, + stdUsers, + new AccessStatus[] + { + AccessStatus.ALLOWED, + AccessStatus.ALLOWED, + AccessStatus.ALLOWED, + AccessStatus.ALLOWED, + AccessStatus.ALLOWED, + AccessStatus.DENIED + }); + + checkCapabilities( + recordFolder_1, + ADD_MODIFY_EVENT_DATES, + stdUsers, + new AccessStatus[] + { + AccessStatus.ALLOWED, + AccessStatus.ALLOWED, + AccessStatus.ALLOWED, + AccessStatus.ALLOWED, + AccessStatus.ALLOWED, + AccessStatus.DENIED + }); + + checkCapabilities( + record_1, + ADD_MODIFY_EVENT_DATES, + stdUsers, + new AccessStatus[] + { + AccessStatus.DENIED, + AccessStatus.DENIED, + AccessStatus.DENIED, + AccessStatus.DENIED, + AccessStatus.DENIED, + AccessStatus.DENIED + }); + + checkCapabilities( + recordFolder_2, + ADD_MODIFY_EVENT_DATES, + stdUsers, + new AccessStatus[] + { + AccessStatus.DENIED, + AccessStatus.DENIED, + AccessStatus.DENIED, + AccessStatus.DENIED, + AccessStatus.DENIED, + AccessStatus.DENIED + }); + + checkCapabilities( + record_2, + ADD_MODIFY_EVENT_DATES, + stdUsers, + new AccessStatus[] + { + AccessStatus.ALLOWED, + AccessStatus.ALLOWED, + AccessStatus.ALLOWED, + AccessStatus.ALLOWED, + AccessStatus.ALLOWED, + AccessStatus.DENIED + }); + + /** Test user has no capabilities */ + checkTestUserCapabilities(ADD_MODIFY_EVENT_DATES, + AccessStatus.DENIED, // recordFolder_1 + AccessStatus.DENIED, // record_1 + AccessStatus.DENIED, // recordFolder_2 + AccessStatus.DENIED); // record_2 + + /** Add filing to both record folders */ + retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.SYSTEM_USER_NAME); + + permissionService.setPermission(filePlan, testers, VIEW_RECORDS, true); + permissionService.setInheritParentPermissions(recordCategory_1, false); + permissionService.setInheritParentPermissions(recordCategory_2, false); + permissionService.setPermission(recordCategory_1, testers, READ_RECORDS, true); + permissionService.setPermission(recordCategory_2, testers, READ_RECORDS, true); + permissionService.setPermission(recordFolder_1, testers, FILING, true); + permissionService.setPermission(recordFolder_2, testers, FILING, true); + + return null; + } + }, false, true); + + /** Check capabilities */ + checkTestUserCapabilities(ADD_MODIFY_EVENT_DATES, + AccessStatus.DENIED, // recordFolder_1 + AccessStatus.DENIED, // record_1 + AccessStatus.DENIED, // recordFolder_2 + AccessStatus.DENIED); // record_2 + + /** Add declare record capability */ + addCapability(DECLARE_RECORDS, testers, filePlan); + checkTestUserCapabilities(ADD_MODIFY_EVENT_DATES, + AccessStatus.DENIED, // recordFolder_1 + AccessStatus.DENIED, // record_1 + AccessStatus.DENIED, // recordFolder_2 + AccessStatus.DENIED); // record_2 + + /** Add modify event date capability */ + addCapability(ADD_MODIFY_EVENT_DATES, testers, filePlan); + checkTestUserCapabilities(ADD_MODIFY_EVENT_DATES, + AccessStatus.ALLOWED, // recordFolder_1 + AccessStatus.DENIED, // record_1 + AccessStatus.DENIED, // recordFolder_2 + AccessStatus.ALLOWED); // record_2 + + /** Remove declare capability */ + removeCapability(DECLARE_RECORDS, testers, filePlan); + checkTestUserCapabilities(ADD_MODIFY_EVENT_DATES, + AccessStatus.ALLOWED, // recordFolder_1 + AccessStatus.DENIED, // record_1 + AccessStatus.DENIED, // recordFolder_2 + AccessStatus.ALLOWED); // record_2 + + /** Add declare capability */ + addCapability(DECLARE_RECORDS, testers, filePlan); + checkTestUserCapabilities(ADD_MODIFY_EVENT_DATES, + AccessStatus.ALLOWED, // recordFolder_1 + AccessStatus.DENIED, // record_1 + AccessStatus.DENIED, // recordFolder_2 + AccessStatus.ALLOWED); // record_2 + + /** Remove view records capability */ + removeCapability(VIEW_RECORDS, testers, filePlan); + checkTestUserCapabilities(ADD_MODIFY_EVENT_DATES, + AccessStatus.DENIED, // recordFolder_1 + AccessStatus.DENIED, // record_1 + AccessStatus.DENIED, // recordFolder_2 + AccessStatus.DENIED); // record_2 + + /** Add view records capability */ + addCapability(VIEW_RECORDS, testers, filePlan); + checkTestUserCapabilities(ADD_MODIFY_EVENT_DATES, + AccessStatus.ALLOWED, // recordFolder_1 + AccessStatus.DENIED, // record_1 + AccessStatus.DENIED, // recordFolder_2 + AccessStatus.ALLOWED); // record_2 + + /** Remove filing from record folders */ + removeCapability(FILING, testers, recordFolder_1, recordFolder_2); + checkTestUserCapabilities(ADD_MODIFY_EVENT_DATES, + AccessStatus.DENIED, // recordFolder_1 + AccessStatus.DENIED, // record_1 + AccessStatus.DENIED, // recordFolder_2 + AccessStatus.DENIED); // record_2 + + /** Set filing permission on records folders */ + addCapability(FILING, testers, recordFolder_1, recordFolder_2); + checkTestUserCapabilities(ADD_MODIFY_EVENT_DATES, + AccessStatus.ALLOWED, // recordFolder_1 + AccessStatus.DENIED, // record_1 + AccessStatus.DENIED, // recordFolder_2 + AccessStatus.ALLOWED); // record_2 + + /** Freeze folder 1 */ + Map params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "one"); + executeAction("freeze", params, recordFolder_1); + checkTestUserCapabilities(ADD_MODIFY_EVENT_DATES, + AccessStatus.DENIED, // recordFolder_1 + AccessStatus.DENIED, // record_1 + AccessStatus.DENIED, // recordFolder_2 + AccessStatus.ALLOWED); // record_2 + + /** Freeze record_2 */ + params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "Two"); + executeAction("freeze", params, record_2); + + checkTestUserCapabilities(ADD_MODIFY_EVENT_DATES, + AccessStatus.DENIED, // recordFolder_1 + AccessStatus.DENIED, // record_1 + AccessStatus.DENIED, // recordFolder_2 + AccessStatus.DENIED); // record_2 + + /** Unfreeze */ + executeAction("unfreeze", recordFolder_1, record_2); + checkTestUserCapabilities(ADD_MODIFY_EVENT_DATES, + AccessStatus.ALLOWED, // recordFolder_1 + AccessStatus.DENIED, // record_1 + AccessStatus.DENIED, // recordFolder_2 + AccessStatus.ALLOWED); // record_2 + + /** Close record folders */ + executeAction("closeRecordFolder", recordFolder_1, recordFolder_2); + checkTestUserCapabilities(ADD_MODIFY_EVENT_DATES, + AccessStatus.ALLOWED, // recordFolder_1 + AccessStatus.DENIED, // record_1 + AccessStatus.DENIED, // recordFolder_2 + AccessStatus.ALLOWED); // record_2 + + /** Open record folders */ + executeAction("openRecordFolder", recordFolder_1, recordFolder_2); + checkTestUserCapabilities(ADD_MODIFY_EVENT_DATES, + AccessStatus.ALLOWED, // recordFolder_1 + AccessStatus.DENIED, // record_1 + AccessStatus.DENIED, // recordFolder_2 + AccessStatus.ALLOWED); // record_2 + + /** Try and complete events*/ + Map eventDetails = new HashMap(3); + eventDetails.put(CompleteEventAction.PARAM_EVENT_NAME, "event"); + eventDetails.put(CompleteEventAction.PARAM_EVENT_COMPLETED_AT, new Date()); + eventDetails.put(CompleteEventAction.PARAM_EVENT_COMPLETED_BY, test_user); + executeAction("completeEvent", eventDetails, test_user, recordFolder_1); + checkExecuteActionFail("completeEvent", eventDetails, test_user, recordFolder_2); + checkExecuteActionFail("completeEvent", eventDetails, test_user, record_1); + executeAction("completeEvent", eventDetails, test_user, record_2); + + /** Check properties can not be set */ + checkSetPropertyFail(record_1, RecordsManagementModel.PROP_EVENT_EXECUTION_COMPLETE, test_user, true); + checkSetPropertyFail(record_1, RecordsManagementModel.PROP_EVENT_EXECUTION_COMPLETED_AT, test_user, new Date()); + checkSetPropertyFail(record_1, RecordsManagementModel.PROP_EVENT_EXECUTION_COMPLETED_AT, test_user, "me"); + + /** Declare and cutoff */ + declare(record_1, record_2); + cutoff(recordFolder_1, record_2); + checkTestUserCapabilities(ADD_MODIFY_EVENT_DATES, + AccessStatus.ALLOWED, // recordFolder_1 + AccessStatus.DENIED, // record_1 + AccessStatus.DENIED, // recordFolder_2 + AccessStatus.ALLOWED); // record_2 + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/ApproveRecordsScheduledForCutoffCapability.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/ApproveRecordsScheduledForCutoffCapability.java new file mode 100644 index 0000000000..06a6b598ae --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/ApproveRecordsScheduledForCutoffCapability.java @@ -0,0 +1,307 @@ +/* + * 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.module.org_alfresco_module_rm.test.capabilities; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.action.impl.FreezeAction; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.cmr.security.AccessStatus; + +/** + * @author Roy Wetherall + */ +public class ApproveRecordsScheduledForCutoffCapability extends BaseTestCapabilities +{ + public void testApproveRecordsScheduledForCutoffCapability() + { + // File plan permissions + checkPermissions(filePlan, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, stdUsers, + AccessStatus.ALLOWED, + AccessStatus.ALLOWED, + AccessStatus.ALLOWED, + AccessStatus.DENIED, + AccessStatus.DENIED, + AccessStatus.DENIED); + + // Not yet eligible + checkCapabilities(recordFolder_1, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, stdUsers, + AccessStatus.DENIED, + AccessStatus.DENIED, + AccessStatus.DENIED, + AccessStatus.DENIED, + AccessStatus.DENIED, + AccessStatus.DENIED); + checkCapabilities(record_1, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, stdUsers, + AccessStatus.DENIED, + AccessStatus.DENIED, + AccessStatus.DENIED, + AccessStatus.DENIED, + AccessStatus.DENIED, + AccessStatus.DENIED); + checkCapabilities(recordFolder_2, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, stdUsers, + AccessStatus.DENIED, + AccessStatus.DENIED, + AccessStatus.DENIED, + AccessStatus.DENIED, + AccessStatus.DENIED, + AccessStatus.DENIED); + checkCapabilities(record_2, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, stdUsers, + AccessStatus.DENIED, + AccessStatus.DENIED, + AccessStatus.DENIED, + AccessStatus.DENIED, + AccessStatus.DENIED, + AccessStatus.DENIED); + + // Set appropriate state - declare records and make eligible + declare(record_1, record_2); + makeEligible(recordFolder_1, record_2); + + checkCapabilities(recordFolder_1, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, stdUsers, + AccessStatus.ALLOWED, + AccessStatus.ALLOWED, + AccessStatus.ALLOWED, + AccessStatus.DENIED, + AccessStatus.DENIED, + AccessStatus.DENIED); + checkCapabilities(record_1, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, stdUsers, + AccessStatus.DENIED, + AccessStatus.DENIED, + AccessStatus.DENIED, + AccessStatus.DENIED, + AccessStatus.DENIED, + AccessStatus.DENIED); + checkCapabilities(recordFolder_2, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, stdUsers, + AccessStatus.DENIED, + AccessStatus.DENIED, + AccessStatus.DENIED, + AccessStatus.DENIED, + AccessStatus.DENIED, + AccessStatus.DENIED); + checkCapabilities(record_2, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, stdUsers, + AccessStatus.ALLOWED, + AccessStatus.ALLOWED, + AccessStatus.ALLOWED, + AccessStatus.DENIED, + AccessStatus.DENIED, + AccessStatus.DENIED); + + checkTestUserCapabilities(APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + AccessStatus.DENIED, // recordFolder_1 + AccessStatus.DENIED, // record_1 + AccessStatus.DENIED, // recordFolder_2 + AccessStatus.DENIED); // record_2 + + retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.SYSTEM_USER_NAME); + + permissionService.setPermission(filePlan, testers, VIEW_RECORDS, true); + permissionService.setInheritParentPermissions(recordCategory_1, false); + permissionService.setInheritParentPermissions(recordCategory_2, false); + permissionService.setPermission(recordCategory_1, testers, READ_RECORDS, true); + permissionService.setPermission(recordCategory_2, testers, READ_RECORDS, true); + permissionService.setPermission(recordFolder_1, testers, FILING, true); + permissionService.setPermission(recordFolder_2, testers, FILING, true); + + return null; + } + }, false, true); + + checkTestUserCapabilities(APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + AccessStatus.DENIED, // recordFolder_1 + AccessStatus.DENIED, // record_1 + AccessStatus.DENIED, // recordFolder_2 + AccessStatus.DENIED); // record_2 + + addCapability(DECLARE_RECORDS, testers, filePlan); + checkTestUserCapabilities(APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + AccessStatus.DENIED, // recordFolder_1 + AccessStatus.DENIED, // record_1 + AccessStatus.DENIED, // recordFolder_2 + AccessStatus.DENIED); // record_2 + + addCapability(APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, testers, filePlan); + checkTestUserCapabilities(APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + AccessStatus.ALLOWED, // recordFolder_1 + AccessStatus.DENIED, // record_1 + AccessStatus.DENIED, // recordFolder_2 + AccessStatus.ALLOWED); // record_2 + + removeCapability(DECLARE_RECORDS, testers, filePlan); + checkTestUserCapabilities(APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + AccessStatus.ALLOWED, // recordFolder_1 + AccessStatus.DENIED, // record_1 + AccessStatus.DENIED, // recordFolder_2 + AccessStatus.ALLOWED); // record_2 + + addCapability(DECLARE_RECORDS, testers, filePlan); + checkTestUserCapabilities(APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + AccessStatus.ALLOWED, // recordFolder_1 + AccessStatus.DENIED, // record_1 + AccessStatus.DENIED, // recordFolder_2 + AccessStatus.ALLOWED); // record_2 + + removeCapability(VIEW_RECORDS, testers, filePlan); + checkTestUserCapabilities(APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + AccessStatus.DENIED, // recordFolder_1 + AccessStatus.DENIED, // record_1 + AccessStatus.DENIED, // recordFolder_2 + AccessStatus.DENIED); // record_2 + + addCapability(VIEW_RECORDS, testers, filePlan); + checkTestUserCapabilities(APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + AccessStatus.ALLOWED, // recordFolder_1 + AccessStatus.DENIED, // record_1 + AccessStatus.DENIED, // recordFolder_2 + AccessStatus.ALLOWED); // record_2 + + removeCapability(FILING, testers, recordFolder_1, recordFolder_2); + checkTestUserCapabilities(APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + AccessStatus.DENIED, // recordFolder_1 + AccessStatus.DENIED, // record_1 + AccessStatus.DENIED, // recordFolder_2 + AccessStatus.DENIED); // record_2 + + addCapability(FILING, testers, recordFolder_1, recordFolder_2); + checkTestUserCapabilities(APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + AccessStatus.ALLOWED, // recordFolder_1 + AccessStatus.DENIED, // record_1 + AccessStatus.DENIED, // recordFolder_2 + AccessStatus.ALLOWED); // record_2 + + // Freeze record folder + Map params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "one"); + executeAction("freeze", params, recordFolder_1); + checkTestUserCapabilities(APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + AccessStatus.DENIED, // recordFolder_1 + AccessStatus.DENIED, // record_1 + AccessStatus.DENIED, // recordFolder_2 + AccessStatus.ALLOWED); // record_2 + + // Freeze record + executeAction("freeze", params, record_2); + checkTestUserCapabilities(APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + AccessStatus.DENIED, // recordFolder_1 + AccessStatus.DENIED, // record_1 + AccessStatus.DENIED, // recordFolder_2 + AccessStatus.DENIED); // record_2 + + // Unfreeze + executeAction("unfreeze", recordFolder_1, record_2); + checkTestUserCapabilities(APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + AccessStatus.ALLOWED, // recordFolder_1 + AccessStatus.DENIED, // record_1 + AccessStatus.DENIED, // recordFolder_2 + AccessStatus.ALLOWED); // record_2 + + // Close folders + executeAction("closeRecordFolder", recordFolder_1, recordFolder_2); + checkTestUserCapabilities(APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + AccessStatus.ALLOWED, // recordFolder_1 + AccessStatus.DENIED, // record_1 + AccessStatus.DENIED, // recordFolder_2 + AccessStatus.ALLOWED); // record_2 + +// +// AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); +// recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "openRecordFolder"); +// recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "openRecordFolder"); +// +// checkCapability(test_user, recordFolder_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.ALLOWED); +// checkCapability(test_user, record_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); +// checkCapability(test_user, recordFolder_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); +// checkCapability(test_user, record_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.ALLOWED); +// +// // try and cut off +// +// AuthenticationUtil.setFullyAuthenticatedUser(test_user); +// recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "cutoff", null); +// try +// { +// recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "cutoff", null); +// fail(); +// } +// catch (AccessDeniedException ade) +// { +// +// } +// try +// { +// recordsManagementActionService.executeRecordsManagementAction(record_1, "cutoff", null); +// fail(); +// } +// catch (AccessDeniedException ade) +// { +// +// } +// recordsManagementActionService.executeRecordsManagementAction(record_2, "cutoff", null); +// +// // check protected properties +// +// try +// { +// publicNodeService.setProperty(record_1, RecordsManagementModel.PROP_CUT_OFF_DATE, new Date()); +// fail(); +// } +// catch (AccessDeniedException ade) +// { +// +// } + + // check cutoff again (it is already cut off) + + // try + // { + // recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "cutoff", null); + // fail(); + // } + // catch (AccessDeniedException ade) + // { + // + // } + // try + // { + // recordsManagementActionService.executeRecordsManagementAction(record_2, "cutoff", null); + // fail(); + // } + // catch (AccessDeniedException ade) + // { + // + // } + + // checkCapability(test_user, recordFolder_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + // AccessStatus.DENIED); + // checkCapability(test_user, record_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + // AccessStatus.DENIED); + // checkCapability(test_user, recordFolder_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + // AccessStatus.DENIED); + // checkCapability(test_user, record_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + // AccessStatus.DENIED); + } + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/BaseCapabilitiesTest.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/BaseCapabilitiesTest.java new file mode 100644 index 0000000000..7c429e4b58 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/BaseCapabilitiesTest.java @@ -0,0 +1,922 @@ +/* + * 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.module.org_alfresco_module_rm.test.capabilities; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.transaction.UserTransaction; + +import junit.framework.TestCase; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService; +import org.alfresco.module.org_alfresco_module_rm.capability.Capability; +import org.alfresco.module.org_alfresco_module_rm.capability.CapabilityService; +import org.alfresco.module.org_alfresco_module_rm.capability.RMEntryVoter; +import org.alfresco.module.org_alfresco_module_rm.capability.RMPermissionModel; +import org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEventService; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.repo.security.permissions.AccessDeniedException; +import org.alfresco.repo.security.permissions.PermissionReference; +import org.alfresco.repo.security.permissions.impl.model.PermissionModel; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +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; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.AuthorityService; +import org.alfresco.service.cmr.security.AuthorityType; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ApplicationContextHelper; +import org.springframework.context.ApplicationContext; + +/** + * @author Roy Wetherall + */ +public abstract class BaseCapabilitiesTest extends TestCase + implements RMPermissionModel, RecordsManagementModel +{ + /* Application context */ + protected ApplicationContext ctx; + + /* Root node reference */ + protected StoreRef storeRef; + protected NodeRef rootNodeRef; + + /* Services */ + protected NodeService nodeService; + protected NodeService publicNodeService; + protected TransactionService transactionService; + protected PermissionService permissionService; + protected RecordsManagementService recordsManagementService; + protected RecordsManagementSecurityService recordsManagementSecurityService; + protected RecordsManagementActionService recordsManagementActionService; + protected RecordsManagementEventService recordsManagementEventService; + protected PermissionModel permissionModel; + protected ContentService contentService; + protected AuthorityService authorityService; + protected PersonService personService; + protected ContentService publicContentService; + protected RetryingTransactionHelper retryingTransactionHelper; + protected CapabilityService capabilityService; + + protected RMEntryVoter rmEntryVoter; + + protected UserTransaction testTX; + + protected NodeRef filePlan; + protected NodeRef recordSeries; + protected NodeRef recordCategory_1; + protected NodeRef recordCategory_2; + protected NodeRef recordFolder_1; + protected NodeRef recordFolder_2; + protected NodeRef record_1; + protected NodeRef record_2; + protected NodeRef recordCategory_3; + protected NodeRef recordFolder_3; + protected NodeRef record_3; + + protected String rmUsers; + protected String rmPowerUsers; + protected String rmSecurityOfficers; + protected String rmRecordsManagers; + protected String rmAdministrators; + + protected String rm_user; + protected String rm_power_user; + protected String rm_security_officer; + protected String rm_records_manager; + protected String rm_administrator; + protected String test_user; + + protected String testers; + + protected String[] stdUsers; + protected NodeRef[] stdNodeRefs;; + + /** + * Test setup + * @throws Exception + */ + protected void setUp() throws Exception + { + // Get the application context + ctx = ApplicationContextHelper.getApplicationContext(); + + // Get beans + nodeService = (NodeService) ctx.getBean("dbNodeService"); + publicNodeService = (NodeService) ctx.getBean("NodeService"); + transactionService = (TransactionService) ctx.getBean("transactionComponent"); + permissionService = (PermissionService) ctx.getBean("permissionService"); + permissionModel = (PermissionModel) ctx.getBean("permissionsModelDAO"); + contentService = (ContentService) ctx.getBean("contentService"); + publicContentService = (ContentService) ctx.getBean("ContentService"); + authorityService = (AuthorityService) ctx.getBean("authorityService"); + personService = (PersonService) ctx.getBean("personService"); + recordsManagementService = (RecordsManagementService) ctx.getBean("RecordsManagementService"); + recordsManagementSecurityService = (RecordsManagementSecurityService) ctx.getBean("RecordsManagementSecurityService"); + recordsManagementActionService = (RecordsManagementActionService) ctx.getBean("RecordsManagementActionService"); + recordsManagementEventService = (RecordsManagementEventService) ctx.getBean("RecordsManagementEventService"); + rmEntryVoter = (RMEntryVoter) ctx.getBean("rmEntryVoter"); + retryingTransactionHelper = (RetryingTransactionHelper)ctx.getBean("retryingTransactionHelper"); + capabilityService = (CapabilityService)ctx.getBean("capabilityService"); + + retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + // As system user + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + + // Create store and get the root node reference + storeRef = nodeService.createStore(StoreRef.PROTOCOL_WORKSPACE, "Test_" + System.currentTimeMillis()); + rootNodeRef = nodeService.getRootNode(storeRef); + + // As admin user + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + + // Create test events + recordsManagementEventService.getEvents(); + recordsManagementEventService.addEvent("rmEventType.simple", "event", "My Event"); + + // Create file plan node + filePlan = nodeService.createNode( + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + TYPE_FILE_PLAN, + TYPE_FILE_PLAN).getChildRef(); + + return null; + } + }, false, true); + + + // Load in the plan data required for the test + loadFilePlanData(); + + retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + // As system user + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + + // create people ... + rm_user = "rm_user_" + storeRef.getIdentifier(); + rm_power_user = "rm_power_user_" + storeRef.getIdentifier(); + rm_security_officer = "rm_security_officer_" + storeRef.getIdentifier(); + rm_records_manager = "rm_records_manager_" + storeRef.getIdentifier(); + rm_administrator = "rm_administrator_" + storeRef.getIdentifier(); + test_user = "test_user_" + storeRef.getIdentifier(); + + personService.createPerson(createDefaultProperties(rm_user)); + personService.createPerson(createDefaultProperties(rm_power_user)); + personService.createPerson(createDefaultProperties(rm_security_officer)); + personService.createPerson(createDefaultProperties(rm_records_manager)); + personService.createPerson(createDefaultProperties(rm_administrator)); + personService.createPerson(createDefaultProperties(test_user)); + + // create roles as groups + rmUsers = authorityService.createAuthority(AuthorityType.GROUP, "RM_USER_" + storeRef.getIdentifier()); + rmPowerUsers = authorityService.createAuthority(AuthorityType.GROUP, "RM_POWER_USER_" + storeRef.getIdentifier()); + rmSecurityOfficers = authorityService.createAuthority(AuthorityType.GROUP, "RM_SECURITY_OFFICER_" + storeRef.getIdentifier()); + rmRecordsManagers = authorityService.createAuthority(AuthorityType.GROUP, "RM_RECORDS_MANAGER_" + storeRef.getIdentifier()); + rmAdministrators = authorityService.createAuthority(AuthorityType.GROUP, "RM_ADMINISTRATOR_" + storeRef.getIdentifier()); + testers = authorityService.createAuthority(AuthorityType.GROUP, "RM_TESTOR_" + storeRef.getIdentifier()); + + authorityService.addAuthority(testers, test_user); + + setPermissions(rmUsers, rm_user, ROLE_USER); + setPermissions(rmPowerUsers, rm_power_user, ROLE_POWER_USER); + setPermissions(rmSecurityOfficers, rm_security_officer, ROLE_SECURITY_OFFICER); + setPermissions(rmRecordsManagers, rm_records_manager, ROLE_RECORDS_MANAGER); + setPermissions(rmAdministrators, rm_administrator, ROLE_ADMINISTRATOR); + + stdUsers = new String[] + { + AuthenticationUtil.getSystemUserName(), + rm_administrator, + rm_records_manager, + rm_security_officer, + rm_power_user, + rm_user + }; + + stdNodeRefs = new NodeRef[] + { + recordFolder_1, + record_1, + recordFolder_2, + record_2 + }; + + return null; + } + }, false, true); + } + + /** + * Test tear down + * @throws Exception + */ + @Override + protected void tearDown() throws Exception + { + // TODO we should clean up as much as we can .... + } + + /** + * Set the permissions for a group, user and role + * @param group + * @param user + * @param role + */ + private void setPermissions(String group, String user, String role) + { + for (PermissionReference pr : permissionModel.getImmediateGranteePermissions(permissionModel.getPermissionReference(null, role))) + { + setPermission(filePlan, group, pr.getName(), true); + } + authorityService.addAuthority(group, user); + setPermission(filePlan, user, FILING, true); + } + + /** + * Loads the file plan date required for the tests + */ + protected void loadFilePlanData() + { + recordSeries = createRecordSeries(filePlan, "RS", "RS-1", "Record Series", "My record series"); + + recordCategory_1 = createRecordCategory(recordSeries, "Docs", "101-1", "Docs", "Docs", "week|1", true, false); + recordCategory_2 = createRecordCategory(recordSeries, "More Docs", "101-2", "More Docs", "More Docs", "week|1", true, true); + recordCategory_3 = createRecordCategory(recordSeries, "No disp schedule", "101-3", "No disp schedule", "No disp schedule", "week|1", true, null); + + recordFolder_1 = createRecordFolder(recordCategory_1, "F1", "101-3", "title", "description", "week|1", true); + recordFolder_2 = createRecordFolder(recordCategory_2, "F2", "102-3", "title", "description", "week|1", true); + recordFolder_3 = createRecordFolder(recordCategory_3, "F3", "103-3", "title", "description", "week|1", true); + + record_1 = createRecord(recordFolder_1); + record_2 = createRecord(recordFolder_2); + record_3 = createRecord(recordFolder_3); + } + + /** + * Set permission for authority on node reference. + * @param nodeRef + * @param authority + * @param permission + * @param allow + */ + private void setPermission(NodeRef nodeRef, String authority, String permission, boolean allow) + { + permissionService.setPermission(nodeRef, authority, permission, allow); + if (permission.equals(FILING)) + { + if (recordsManagementService.isRecordCategory(nodeRef) == true) + { + List assocs = nodeService.getChildAssocs(nodeRef, ContentModel.ASSOC_CONTAINS, RegexQNamePattern.MATCH_ALL); + for (ChildAssociationRef assoc : assocs) + { + NodeRef child = assoc.getChildRef(); + if (recordsManagementService.isRecordFolder(child) == true || + recordsManagementService.isRecordCategory(child) == true) + { + setPermission(child, authority, permission, allow); + } + } + } + } + } + + /** + * Create the default person properties + * @param userName + * @return + */ + private Map createDefaultProperties(String userName) + { + HashMap properties = new HashMap(); + properties.put(ContentModel.PROP_USERNAME, userName); + properties.put(ContentModel.PROP_HOMEFOLDER, null); + properties.put(ContentModel.PROP_FIRSTNAME, userName); + properties.put(ContentModel.PROP_LASTNAME, userName); + properties.put(ContentModel.PROP_EMAIL, userName); + properties.put(ContentModel.PROP_ORGID, ""); + return properties; + } + + /** + * Create a new record. Executed in a new transaction. + */ + private NodeRef createRecord(final NodeRef recordFolder) + { + return retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public NodeRef execute() throws Throwable + { + // As admin + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + + // Create the record + Map props = new HashMap(1); + props.put(ContentModel.PROP_NAME, "MyRecord.txt"); + NodeRef recordOne = nodeService.createNode(recordFolder, ContentModel.ASSOC_CONTAINS, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "MyRecord.txt"), + ContentModel.TYPE_CONTENT, props).getChildRef(); + + // Set the content + ContentWriter writer = contentService.getWriter(recordOne, ContentModel.PROP_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.setEncoding("UTF-8"); + writer.putContent("There is some content in this record"); + return recordOne; + } + }, false, true); + } + + /** + * Create a test record series. Executed in a new transaction. + */ + private NodeRef createRecordSeries(final NodeRef filePlan, final String name, final String identifier, final String title, final String description) + { + return retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public NodeRef execute() throws Throwable + { + // As admin + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + + HashMap properties = new HashMap(); + properties.put(ContentModel.PROP_NAME, name); + properties.put(PROP_IDENTIFIER, identifier); + properties.put(ContentModel.PROP_TITLE, title); + properties.put(ContentModel.PROP_DESCRIPTION, description); + + NodeRef recordSeried = nodeService.createNode(filePlan, ContentModel.ASSOC_CONTAINS, TYPE_RECORD_CATEGORY, TYPE_RECORD_CATEGORY, properties).getChildRef(); + permissionService.setInheritParentPermissions(recordSeried, false); + + return recordSeried; + } + }, false, true); + } + + /** + * Create a test record category in a new transaction. + */ + private NodeRef createRecordCategory( + final NodeRef recordSeries, + final String name, + final String identifier, + final String title, + final String description, + final String review, + final boolean vital, + final Boolean recordLevelDisposition) + { + return retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public NodeRef execute() throws Throwable + { + // As admin + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + + HashMap properties = new HashMap(); + properties.put(ContentModel.PROP_NAME, name); + properties.put(PROP_IDENTIFIER, identifier); + properties.put(ContentModel.PROP_TITLE, title); + properties.put(ContentModel.PROP_DESCRIPTION, description); + properties.put(PROP_REVIEW_PERIOD, review); + properties.put(PROP_VITAL_RECORD_INDICATOR, vital); + + NodeRef answer = nodeService.createNode(recordSeries, ContentModel.ASSOC_CONTAINS, TYPE_RECORD_CATEGORY, TYPE_RECORD_CATEGORY, properties) + .getChildRef(); + + if (recordLevelDisposition != null) + { + properties = new HashMap(); + properties.put(PROP_DISPOSITION_AUTHORITY, "N1-218-00-4 item 023"); + properties.put(PROP_DISPOSITION_INSTRUCTIONS, "Cut off monthly, hold 1 month, then destroy."); + properties.put(PROP_RECORD_LEVEL_DISPOSITION, recordLevelDisposition); + NodeRef ds = nodeService.createNode(answer, ASSOC_DISPOSITION_SCHEDULE, TYPE_DISPOSITION_SCHEDULE, TYPE_DISPOSITION_SCHEDULE, + properties).getChildRef(); + + createDispoistionAction(ds, "cutoff", "monthend|1", null, "event"); + createDispoistionAction(ds, "transfer", "month|1", null, null); + createDispoistionAction(ds, "accession", "month|1", null, null); + createDispoistionAction(ds, "destroy", "month|1", "{http://www.alfresco.org/model/recordsmanagement/1.0}cutOffDate", null); + } + + permissionService.setInheritParentPermissions(answer, false); + + return answer; + } + }, false, true); + } + + /** + * Create disposition action. + * @param disposition + * @param actionName + * @param period + * @param periodProperty + * @param event + * @return + */ + private NodeRef createDispoistionAction(NodeRef disposition, String actionName, String period, String periodProperty, String event) + { + HashMap properties = new HashMap(); + properties.put(PROP_DISPOSITION_ACTION_NAME, actionName); + properties.put(PROP_DISPOSITION_PERIOD, period); + if (periodProperty != null) + { + properties.put(PROP_DISPOSITION_PERIOD_PROPERTY, periodProperty); + } + if (event != null) + { + properties.put(PROP_DISPOSITION_EVENT, event); + } + NodeRef answer = nodeService.createNode(disposition, ASSOC_DISPOSITION_ACTION_DEFINITIONS, TYPE_DISPOSITION_ACTION_DEFINITION, + TYPE_DISPOSITION_ACTION_DEFINITION, properties).getChildRef(); + return answer; + } + + /** + * Create record folder. Executed in a new transaction. + * @param recordCategory + * @param name + * @param identifier + * @param title + * @param description + * @param review + * @param vital + * @return + */ + private NodeRef createRecordFolder( + final NodeRef recordCategory, + final String name, + final String identifier, + final String title, + final String description, + final String review, + final boolean vital) + { + return retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public NodeRef execute() throws Throwable + { + // As admin + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + + HashMap properties = new HashMap(); + properties.put(ContentModel.PROP_NAME, name); + properties.put(PROP_IDENTIFIER, identifier); + properties.put(ContentModel.PROP_TITLE, title); + properties.put(ContentModel.PROP_DESCRIPTION, description); + properties.put(PROP_REVIEW_PERIOD, review); + properties.put(PROP_VITAL_RECORD_INDICATOR, vital); + NodeRef answer = nodeService.createNode(recordCategory, ContentModel.ASSOC_CONTAINS, TYPE_RECORD_FOLDER, TYPE_RECORD_FOLDER, properties) + .getChildRef(); + permissionService.setInheritParentPermissions(answer, false); + return answer; + } + }, false, true); + } + + /** + * + * @param user + * @param nodeRef + * @param capabilityName + * @param accessStstus + */ + protected void checkCapability(final String user, final NodeRef nodeRef, final String capabilityName, final AccessStatus expected) + { + AuthenticationUtil.runAs(new RunAsWork() + { + @Override + public Object doWork() throws Exception + { + Capability capability = recordsManagementSecurityService.getCapability(capabilityName); + assertNotNull(capability); + + List capabilities = new ArrayList(1); + capabilities.add(capabilityName); + Map access = capabilityService.getCapabilitiesAccessState(nodeRef, capabilities); + + AccessStatus actual = access.get(capability); + + assertEquals( + "for user: " + user, + expected, + actual); + + return null; + } + }, user); + } + + /** + * + * @param access + * @param name + * @param accessStatus + */ + protected void check(Map access, String name, AccessStatus accessStatus) + { + Capability capability = recordsManagementSecurityService.getCapability(name); + assertNotNull(capability); + assertEquals(accessStatus, access.get(capability)); + } + + /** + * + * @param user + * @param nodeRef + * @param permission + * @param accessStstus + */ + protected void checkPermission(final String user, final NodeRef nodeRef, final String permission, final AccessStatus accessStstus) + { + AuthenticationUtil.runAs(new RunAsWork() + { + @Override + public Object doWork() throws Exception + { + AccessStatus actualAccessStatus = permissionService.hasPermission(nodeRef, permission); + assertTrue(actualAccessStatus == accessStstus); + return null; + } + }, user); + } + + /** + * + * @param nodeRef + * @param permission + * @param users + * @param expectedAccessStatus + */ + protected void checkPermissions( + final NodeRef nodeRef, + final String permission, + final String[] users, + final AccessStatus ... expectedAccessStatus) + { + retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + assertEquals( + "The number of users should match the number of expected access status", + users.length, + expectedAccessStatus.length); + + for (int i = 0; i < users.length; i++) + { + checkPermission(users[i], nodeRef, permission, expectedAccessStatus[i]); + } + + return null; + } + }, true, true); + } + + /** + * + * @param nodeRef + * @param capability + * @param users + * @param expectedAccessStatus + */ + protected void checkCapabilities( + final NodeRef nodeRef, + final String capability, + final String[] users, + final AccessStatus ... expectedAccessStatus) + { + retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + assertEquals( + "The number of users should match the number of expected access status", + users.length, + expectedAccessStatus.length); + + for (int i = 0; i < users.length; i++) + { + checkCapability(users[i], nodeRef, capability, expectedAccessStatus[i]); + } + + return null; + } + }, true, true); + } + + /** + * + * @param user + * @param capability + * @param nodeRefs + * @param expectedAccessStatus + */ + protected void checkCapabilities( + final String user, + final String capability, + final NodeRef[] nodeRefs, + final AccessStatus ... expectedAccessStatus) + { + retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + assertEquals( + "The number of node references should match the number of expected access status", + nodeRefs.length, + expectedAccessStatus.length); + + for (int i = 0; i < nodeRefs.length; i++) + { + checkCapability(user, nodeRefs[i], capability, expectedAccessStatus[i]); + } + + return null; + } + }, true, true); + } + + /** + * + * @param capability + * @param accessStatus + */ + protected void checkTestUserCapabilities(String capability, AccessStatus ... accessStatus) + { + checkCapabilities( + test_user, + capability, + stdNodeRefs, + accessStatus); + } + + /** + * Execute RM action + * @param action + * @param params + * @param nodeRefs + */ + protected void executeAction(final String action, final Map params, final String user, final NodeRef ... nodeRefs) + { + retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil.setFullyAuthenticatedUser(user); + + for (NodeRef nodeRef : nodeRefs) + { + recordsManagementActionService.executeRecordsManagementAction(nodeRef, action, params); + } + + return null; + } + }, false, true); + } + + /** + * + * @param action + * @param nodeRefs + */ + protected void executeAction(final String action, final NodeRef ... nodeRefs) + { + executeAction(action, null, AuthenticationUtil.SYSTEM_USER_NAME, nodeRefs); + } + + /** + * + * @param action + * @param params + * @param nodeRefs + */ + protected void executeAction(final String action, final Map params, final NodeRef ... nodeRefs) + { + executeAction(action, params, AuthenticationUtil.SYSTEM_USER_NAME, nodeRefs); + } + + /** + * + * @param action + * @param params + * @param user + * @param nodeRefs + */ + protected void checkExecuteActionFail(final String action, final Map params, final String user, final NodeRef ... nodeRefs) + { + try + { + executeAction(action, params, user, nodeRefs); + fail("Action " + action + " has succeded and was expected to fail"); + } + catch (AccessDeniedException ade) + {} + } + + /** + * + * @param nodeRef + * @param property + * @param user + */ + protected void checkSetPropertyFail(final NodeRef nodeRef, final QName property, final String user, final Serializable value) + { + retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil.setFullyAuthenticatedUser(user); + + try + { + publicNodeService.setProperty(nodeRef, property, value); + fail("Expected failure when setting property"); + } + catch (AccessDeniedException ade) + {} + + return null; + } + }, false, true); + } + + /** + * Add a capability + * @param capability + * @param authority + * @param nodeRefs + */ + protected void addCapability(final String capability, final String authority, final NodeRef ... nodeRefs) + { + retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.SYSTEM_USER_NAME); + for (NodeRef nodeRef : nodeRefs) + { + permissionService.setPermission(nodeRef, authority, capability, true); + } + return null; + } + }, false, true); + } + + /** + * Remove capability + * @param capability + * @param authority + * @param nodeRef + */ + protected void removeCapability(final String capability, final String authority, final NodeRef ... nodeRefs) + { + retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.SYSTEM_USER_NAME); + for (NodeRef nodeRef : nodeRefs) + { + permissionService.deletePermission(nodeRef, authority, capability); + } + return null; + } + }, false, true); + } + + /** + * + * @param nodeRefs + */ + protected void declare(final NodeRef ... nodeRefs) + { + retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.SYSTEM_USER_NAME); + + for (NodeRef nodeRef : nodeRefs) + { + nodeService.setProperty(nodeRef, RecordsManagementModel.PROP_ORIGINATOR, "origValue"); + nodeService.setProperty(nodeRef, RecordsManagementModel.PROP_ORIGINATING_ORGANIZATION, "origOrgValue"); + nodeService.setProperty(nodeRef, RecordsManagementModel.PROP_PUBLICATION_DATE, new Date()); + nodeService.setProperty(nodeRef, ContentModel.PROP_TITLE, "titleValue"); + recordsManagementActionService.executeRecordsManagementAction(nodeRef, "declareRecord"); + } + + return null; + } + }, false, true); + } + + protected void cutoff(final NodeRef ... nodeRefs) + { + retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.SYSTEM_USER_NAME); + + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.HOUR, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + + for (NodeRef nodeRef : nodeRefs) + { + NodeRef ndNodeRef = nodeService.getChildAssocs(nodeRef, RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + nodeService.setProperty(ndNodeRef, RecordsManagementModel.PROP_DISPOSITION_AS_OF, calendar.getTime()); + recordsManagementActionService.executeRecordsManagementAction(nodeRef, "cutoff", null); + } + + return null; + } + }, false, true); + } + + protected void makeEligible(final NodeRef ... nodeRefs) + { + retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.SYSTEM_USER_NAME); + + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.HOUR, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + + for (NodeRef nodeRef : nodeRefs) + { + NodeRef ndNodeRef = nodeService.getChildAssocs(nodeRef, RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + nodeService.setProperty(ndNodeRef, RecordsManagementModel.PROP_DISPOSITION_AS_OF, calendar.getTime()); + } + + return null; + } + }, false, true); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/BaseTestCapabilities.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/BaseTestCapabilities.java new file mode 100644 index 0000000000..dc3a734c99 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/BaseTestCapabilities.java @@ -0,0 +1,903 @@ +/* + * 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.module.org_alfresco_module_rm.test.capabilities; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.transaction.UserTransaction; + +import junit.framework.TestCase; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService; +import org.alfresco.module.org_alfresco_module_rm.capability.Capability; +import org.alfresco.module.org_alfresco_module_rm.capability.CapabilityService; +import org.alfresco.module.org_alfresco_module_rm.capability.RMEntryVoter; +import org.alfresco.module.org_alfresco_module_rm.capability.RMPermissionModel; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService; +import org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEventService; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.repo.security.permissions.AccessDeniedException; +import org.alfresco.repo.security.permissions.impl.model.PermissionModel; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +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; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.AuthorityService; +import org.alfresco.service.cmr.security.AuthorityType; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ApplicationContextHelper; +import org.springframework.context.ApplicationContext; + +/** + * @author Roy Wetherall + */ +public abstract class BaseTestCapabilities extends TestCase + implements RMPermissionModel, RecordsManagementModel +{ + /* Application context */ + protected ApplicationContext ctx; + + /* Root node reference */ + protected StoreRef storeRef; + protected NodeRef rootNodeRef; + + /* Services */ + protected NodeService nodeService; + protected NodeService publicNodeService; + protected TransactionService transactionService; + protected PermissionService permissionService; + protected RecordsManagementService recordsManagementService; + protected RecordsManagementSecurityService recordsManagementSecurityService; + protected RecordsManagementActionService recordsManagementActionService; + protected RecordsManagementEventService recordsManagementEventService; + protected DispositionService dispositionService; + protected CapabilityService capabilityService; + protected PermissionModel permissionModel; + protected ContentService contentService; + protected AuthorityService authorityService; + protected PersonService personService; + protected ContentService publicContentService; + protected RetryingTransactionHelper retryingTransactionHelper; + + protected RMEntryVoter rmEntryVoter; + + protected UserTransaction testTX; + + protected NodeRef filePlan; + protected NodeRef recordSeries; + protected NodeRef recordCategory_1; + protected NodeRef recordCategory_2; + protected NodeRef recordFolder_1; + protected NodeRef recordFolder_2; + protected NodeRef record_1; + protected NodeRef record_2; + protected NodeRef recordCategory_3; + protected NodeRef recordFolder_3; + protected NodeRef record_3; + + // protected String rmUsers; + // protected String rmPowerUsers; + // protected String rmSecurityOfficers; + // protected String rmRecordsManagers; + // protected String rmAdministrators; + + protected String rm_user; + protected String rm_power_user; + protected String rm_security_officer; + protected String rm_records_manager; + protected String rm_administrator; + protected String test_user; + + protected String testers; + + protected String[] stdUsers; + protected NodeRef[] stdNodeRefs;; + + /** + * Test setup + * @throws Exception + */ + protected void setUp() throws Exception + { + // Get the application context + ctx = ApplicationContextHelper.getApplicationContext(); + + // Get beans + nodeService = (NodeService) ctx.getBean("dbNodeService"); + publicNodeService = (NodeService) ctx.getBean("NodeService"); + transactionService = (TransactionService) ctx.getBean("transactionComponent"); + permissionService = (PermissionService) ctx.getBean("permissionService"); + permissionModel = (PermissionModel) ctx.getBean("permissionsModelDAO"); + contentService = (ContentService) ctx.getBean("contentService"); + publicContentService = (ContentService) ctx.getBean("ContentService"); + authorityService = (AuthorityService) ctx.getBean("authorityService"); + personService = (PersonService) ctx.getBean("personService"); + capabilityService = (CapabilityService)ctx.getBean("CapabilityService"); + dispositionService = (DispositionService)ctx.getBean("DispositionService"); + recordsManagementService = (RecordsManagementService) ctx.getBean("RecordsManagementService"); + recordsManagementSecurityService = (RecordsManagementSecurityService) ctx.getBean("RecordsManagementSecurityService"); + recordsManagementActionService = (RecordsManagementActionService) ctx.getBean("RecordsManagementActionService"); + recordsManagementEventService = (RecordsManagementEventService) ctx.getBean("RecordsManagementEventService"); + rmEntryVoter = (RMEntryVoter) ctx.getBean("rmEntryVoter"); + retryingTransactionHelper = (RetryingTransactionHelper)ctx.getBean("retryingTransactionHelper"); + + retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + // As system user + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + + // Create store and get the root node reference + storeRef = nodeService.createStore(StoreRef.PROTOCOL_WORKSPACE, "Test_" + System.currentTimeMillis()); + rootNodeRef = nodeService.getRootNode(storeRef); + + // As admin user + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + + // Create test events + recordsManagementEventService.getEvents(); + recordsManagementEventService.addEvent("rmEventType.simple", "event", "My Event"); + + // Create file plan node + filePlan = nodeService.createNode( + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + TYPE_FILE_PLAN, + TYPE_FILE_PLAN).getChildRef(); + + return null; + } + }, false, true); + + + // Load in the plan data required for the test + loadFilePlanData(); + + retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + // As system user + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + + // create people ... + rm_user = "rm_user_" + storeRef.getIdentifier(); + rm_power_user = "rm_power_user_" + storeRef.getIdentifier(); + rm_security_officer = "rm_security_officer_" + storeRef.getIdentifier(); + rm_records_manager = "rm_records_manager_" + storeRef.getIdentifier(); + rm_administrator = "rm_administrator_" + storeRef.getIdentifier(); + test_user = "test_user_" + storeRef.getIdentifier(); + + personService.createPerson(createDefaultProperties(rm_user)); + personService.createPerson(createDefaultProperties(rm_power_user)); + personService.createPerson(createDefaultProperties(rm_security_officer)); + personService.createPerson(createDefaultProperties(rm_records_manager)); + personService.createPerson(createDefaultProperties(rm_administrator)); + personService.createPerson(createDefaultProperties(test_user)); + + // create roles as groups +// rmUsers = authorityService.createAuthority(AuthorityType.GROUP, "RM_USER_" + storeRef.getIdentifier()); +// rmPowerUsers = authorityService.createAuthority(AuthorityType.GROUP, "RM_POWER_USER_" + storeRef.getIdentifier()); +// rmSecurityOfficers = authorityService.createAuthority(AuthorityType.GROUP, "RM_SECURITY_OFFICER_" + storeRef.getIdentifier()); +// rmRecordsManagers = authorityService.createAuthority(AuthorityType.GROUP, "RM_RECORDS_MANAGER_" + storeRef.getIdentifier()); +// rmAdministrators = authorityService.createAuthority(AuthorityType.GROUP, "RM_ADMINISTRATOR_" + storeRef.getIdentifier()); + + testers = authorityService.createAuthority(AuthorityType.GROUP, "RM_TESTOR_" + storeRef.getIdentifier()); + authorityService.addAuthority(testers, test_user); + + // rmUsers = recordsManagementSecurityService.assignRoleToAuthority(filePlan, ROLE, rm_user); + + + setPermissions(rm_user, ROLE_NAME_USER); + setPermissions(rm_power_user, ROLE_NAME_POWER_USER); + setPermissions(rm_security_officer, ROLE_NAME_SECURITY_OFFICER); + setPermissions(rm_records_manager, ROLE_NAME_RECORDS_MANAGER); + setPermissions(rm_administrator, ROLE_NAME_ADMINISTRATOR); + + stdUsers = new String[] + { + AuthenticationUtil.getSystemUserName(), + rm_administrator, + rm_records_manager, + rm_security_officer, + rm_power_user, + rm_user + }; + + stdNodeRefs = new NodeRef[] + { + recordFolder_1, + record_1, + recordFolder_2, + record_2 + }; + + return null; + } + }, false, true); + } + + /** + * Test tear down + * @throws Exception + */ + @Override + protected void tearDown() throws Exception + { + // TODO we should clean up as much as we can .... + } + + /** + * Set the permissions for a group, user and role + * @param group + * @param user + * @param role + */ + private void setPermissions(String user, String role) + { + recordsManagementSecurityService.assignRoleToAuthority(filePlan, role, user); + recordsManagementSecurityService.setPermission(filePlan, user, FILING); + } + + /** + * Loads the file plan date required for the tests + */ + protected void loadFilePlanData() + { + recordSeries = createRecordSeries(filePlan, "RS", "Record Series", "My record series"); + + recordCategory_1 = createRecordCategory(recordSeries, "Docs", "Docs", "Docs", "week|1", true, false); + recordCategory_2 = createRecordCategory(recordSeries, "More Docs", "More Docs", "More Docs", "week|1", true, true); + recordCategory_3 = createRecordSeries(recordSeries, "No Dis", "No disp schedule", "No disp schedule"); + + recordFolder_1 = createRecordFolder(recordCategory_1, "F1", "title", "description"); + recordFolder_2 = createRecordFolder(recordCategory_2, "F2", "title", "description"); + recordFolder_3 = createRecordFolder(recordCategory_3, "F3", "title", "description"); + + record_1 = createRecord(recordFolder_1); + record_2 = createRecord(recordFolder_2); + record_3 = createRecord(recordFolder_3); + } + + /** + * Set permission for authority on node reference. + * @param nodeRef + * @param authority + * @param permission + * @param allow + */ +// private void setPermission(NodeRef nodeRef, String authority, String permission, boolean allow) +// { +// permissionService.setPermission(nodeRef, authority, permission, allow); +// if (permission.equals(FILING)) +// { +// if (recordsManagementService.isRecordCategory(nodeRef) == true) +// { +// List assocs = nodeService.getChildAssocs(nodeRef, ContentModel.ASSOC_CONTAINS, RegexQNamePattern.MATCH_ALL); +// for (ChildAssociationRef assoc : assocs) +// { +// NodeRef child = assoc.getChildRef(); +// if (recordsManagementService.isRecordFolder(child) == true || +// recordsManagementService.isRecordCategory(child) == true) +// { +// setPermission(child, authority, permission, allow); +// } +// } +// } +// } +// } + + /** + * Create the default person properties + * @param userName + * @return + */ + private Map createDefaultProperties(String userName) + { + HashMap properties = new HashMap(); + properties.put(ContentModel.PROP_USERNAME, userName); + properties.put(ContentModel.PROP_HOMEFOLDER, null); + properties.put(ContentModel.PROP_FIRSTNAME, userName); + properties.put(ContentModel.PROP_LASTNAME, userName); + properties.put(ContentModel.PROP_EMAIL, userName); + properties.put(ContentModel.PROP_ORGID, ""); + return properties; + } + + /** + * Create a new record. Executed in a new transaction. + */ + private NodeRef createRecord(final NodeRef recordFolder) + { + return retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public NodeRef execute() throws Throwable + { + // As admin + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + + // Create the record + Map props = new HashMap(1); + props.put(ContentModel.PROP_NAME, "MyRecord.txt"); + NodeRef recordOne = nodeService.createNode(recordFolder, ContentModel.ASSOC_CONTAINS, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "MyRecord.txt"), + ContentModel.TYPE_CONTENT, props).getChildRef(); + + // Set the content + ContentWriter writer = contentService.getWriter(recordOne, ContentModel.PROP_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.setEncoding("UTF-8"); + writer.putContent("There is some content in this record"); + return recordOne; + } + }, false, true); + } + + /** + * Create a test record series. Executed in a new transaction. + */ + private NodeRef createRecordSeries(final NodeRef filePlan, final String name, final String title, final String description) + { + return retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public NodeRef execute() throws Throwable + { + // As admin + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + + HashMap properties = new HashMap(); + properties.put(ContentModel.PROP_TITLE, title); + properties.put(ContentModel.PROP_DESCRIPTION, description); + + return recordsManagementService.createRecordCategory(filePlan, name, properties); + + } + }, false, true); + } + + /** + * Create a test record category in a new transaction. + */ + private NodeRef createRecordCategory( + final NodeRef recordSeries, + final String name, + final String title, + final String description, + final String review, + final boolean vital, + final boolean recordLevelDisposition) + { + return retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public NodeRef execute() throws Throwable + { + // As admin + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + + HashMap properties = new HashMap(); + properties.put(ContentModel.PROP_TITLE, title); + properties.put(ContentModel.PROP_DESCRIPTION, description); + + if (vital == true) + { + properties.put(PROP_REVIEW_PERIOD, review); + properties.put(PROP_VITAL_RECORD_INDICATOR, vital); + } + + NodeRef rc = recordsManagementService.createRecordCategory(recordSeries, name, properties); + + properties = new HashMap(); + properties.put(PROP_DISPOSITION_AUTHORITY, "N1-218-00-4 item 023"); + properties.put(PROP_DISPOSITION_INSTRUCTIONS, "Cut off monthly, hold 1 month, then destroy."); + properties.put(PROP_RECORD_LEVEL_DISPOSITION, recordLevelDisposition); + + DispositionSchedule ds = dispositionService.createDispositionSchedule(rc, properties); + + addDispositionAction(ds, "cutoff", "monthend|1", null, "event"); + addDispositionAction(ds, "transfer", "month|1", null, null); + addDispositionAction(ds, "accession", "month|1", null, null); + addDispositionAction(ds, "destroy", "month|1", "{http://www.alfresco.org/model/recordsmanagement/1.0}cutOffDate", null); + + return rc; + } + }, false, true); + } + + /** + * Create disposition action. + * @param disposition + * @param actionName + * @param period + * @param periodProperty + * @param event + * @return + */ + private void addDispositionAction(DispositionSchedule disposition, String actionName, String period, String periodProperty, String event) + { + HashMap properties = new HashMap(); + properties.put(PROP_DISPOSITION_ACTION_NAME, actionName); + properties.put(PROP_DISPOSITION_PERIOD, period); + if (periodProperty != null) + { + properties.put(PROP_DISPOSITION_PERIOD_PROPERTY, periodProperty); + } + if (event != null) + { + properties.put(PROP_DISPOSITION_EVENT, event); + } + dispositionService.addDispositionActionDefinition(disposition, properties); + } + + /** + * Create record folder. Executed in a new transaction. + * @param recordCategory + * @param name + * @param identifier + * @param title + * @param description + * @param review + * @param vital + * @return + */ + private NodeRef createRecordFolder( + final NodeRef recordCategory, + final String name, + final String title, + final String description) + { + return retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public NodeRef execute() throws Throwable + { + // As admin + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + + HashMap properties = new HashMap(); + properties.put(ContentModel.PROP_TITLE, title); + properties.put(ContentModel.PROP_DESCRIPTION, description); + + return recordsManagementService.createRecordFolder(recordCategory, name, properties); + } + }, false, true); + } + + /** + * + * @param user + * @param nodeRef + * @param capabilityName + * @param accessStstus + */ + protected void checkCapability(final String user, final NodeRef nodeRef, final String capabilityName, final AccessStatus expected) + { + AuthenticationUtil.runAs(new RunAsWork() + { + @Override + public Object doWork() throws Exception + { + Capability capability = recordsManagementSecurityService.getCapability(capabilityName); + assertNotNull(capability); + + List capabilities = new ArrayList(1); + capabilities.add(capabilityName); + Map access = capabilityService.getCapabilitiesAccessState(nodeRef, capabilities); + + AccessStatus actual = access.get(capability); + + assertEquals( + "for user: " + user, + expected, + actual); + + return null; + } + }, user); + } + + /** + * + * @param access + * @param name + * @param accessStatus + */ + protected void check(Map access, String name, AccessStatus accessStatus) + { + Capability capability = recordsManagementSecurityService.getCapability(name); + assertNotNull(capability); + assertEquals(accessStatus, access.get(capability)); + } + + /** + * + * @param user + * @param nodeRef + * @param permission + * @param accessStstus + */ + protected void checkPermission(final String user, final NodeRef nodeRef, final String permission, final AccessStatus accessStstus) + { + AuthenticationUtil.runAs(new RunAsWork() + { + @Override + public Object doWork() throws Exception + { + AccessStatus actualAccessStatus = permissionService.hasPermission(nodeRef, permission); + assertTrue(actualAccessStatus == accessStstus); + return null; + } + }, user); + } + + /** + * + * @param nodeRef + * @param permission + * @param users + * @param expectedAccessStatus + */ + protected void checkPermissions( + final NodeRef nodeRef, + final String permission, + final String[] users, + final AccessStatus ... expectedAccessStatus) + { + retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + assertEquals( + "The number of users should match the number of expected access status", + users.length, + expectedAccessStatus.length); + + for (int i = 0; i < users.length; i++) + { + checkPermission(users[i], nodeRef, permission, expectedAccessStatus[i]); + } + + return null; + } + }, true, true); + } + + /** + * + * @param nodeRef + * @param capability + * @param users + * @param expectedAccessStatus + */ + protected void checkCapabilities( + final NodeRef nodeRef, + final String capability, + final String[] users, + final AccessStatus ... expectedAccessStatus) + { + retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + assertEquals( + "The number of users should match the number of expected access status", + users.length, + expectedAccessStatus.length); + + for (int i = 0; i < users.length; i++) + { + checkCapability(users[i], nodeRef, capability, expectedAccessStatus[i]); + } + + return null; + } + }, true, true); + } + + /** + * + * @param user + * @param capability + * @param nodeRefs + * @param expectedAccessStatus + */ + protected void checkCapabilities( + final String user, + final String capability, + final NodeRef[] nodeRefs, + final AccessStatus ... expectedAccessStatus) + { + retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + assertEquals( + "The number of node references should match the number of expected access status", + nodeRefs.length, + expectedAccessStatus.length); + + for (int i = 0; i < nodeRefs.length; i++) + { + checkCapability(user, nodeRefs[i], capability, expectedAccessStatus[i]); + } + + return null; + } + }, true, true); + } + + /** + * + * @param capability + * @param accessStatus + */ + protected void checkTestUserCapabilities(String capability, AccessStatus ... accessStatus) + { + checkCapabilities( + test_user, + capability, + stdNodeRefs, + accessStatus); + } + + /** + * Execute RM action + * @param action + * @param params + * @param nodeRefs + */ + protected void executeAction(final String action, final Map params, final String user, final NodeRef ... nodeRefs) + { + retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil.setFullyAuthenticatedUser(user); + + for (NodeRef nodeRef : nodeRefs) + { + recordsManagementActionService.executeRecordsManagementAction(nodeRef, action, params); + } + + return null; + } + }, false, true); + } + + /** + * + * @param action + * @param nodeRefs + */ + protected void executeAction(final String action, final NodeRef ... nodeRefs) + { + executeAction(action, null, AuthenticationUtil.SYSTEM_USER_NAME, nodeRefs); + } + + /** + * + * @param action + * @param params + * @param nodeRefs + */ + protected void executeAction(final String action, final Map params, final NodeRef ... nodeRefs) + { + executeAction(action, params, AuthenticationUtil.SYSTEM_USER_NAME, nodeRefs); + } + + /** + * + * @param action + * @param params + * @param user + * @param nodeRefs + */ + protected void checkExecuteActionFail(final String action, final Map params, final String user, final NodeRef ... nodeRefs) + { + try + { + executeAction(action, params, user, nodeRefs); + fail("Action " + action + " has succeded and was expected to fail"); + } + catch (AccessDeniedException ade) + {} + } + + /** + * + * @param nodeRef + * @param property + * @param user + */ + protected void checkSetPropertyFail(final NodeRef nodeRef, final QName property, final String user, final Serializable value) + { + retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil.setFullyAuthenticatedUser(user); + + try + { + publicNodeService.setProperty(nodeRef, property, value); + fail("Expected failure when setting property"); + } + catch (AccessDeniedException ade) + {} + + return null; + } + }, false, true); + } + + /** + * Add a capability + * @param capability + * @param authority + * @param nodeRefs + */ + protected void addCapability(final String capability, final String authority, final NodeRef ... nodeRefs) + { + retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.SYSTEM_USER_NAME); + for (NodeRef nodeRef : nodeRefs) + { + permissionService.setPermission(nodeRef, authority, capability, true); + } + return null; + } + }, false, true); + } + + /** + * Remove capability + * @param capability + * @param authority + * @param nodeRef + */ + protected void removeCapability(final String capability, final String authority, final NodeRef ... nodeRefs) + { + retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.SYSTEM_USER_NAME); + for (NodeRef nodeRef : nodeRefs) + { + permissionService.deletePermission(nodeRef, authority, capability); + } + return null; + } + }, false, true); + } + + /** + * + * @param nodeRefs + */ + protected void declare(final NodeRef ... nodeRefs) + { + retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.SYSTEM_USER_NAME); + + for (NodeRef nodeRef : nodeRefs) + { + nodeService.setProperty(nodeRef, RecordsManagementModel.PROP_ORIGINATOR, "origValue"); + nodeService.setProperty(nodeRef, RecordsManagementModel.PROP_ORIGINATING_ORGANIZATION, "origOrgValue"); + nodeService.setProperty(nodeRef, RecordsManagementModel.PROP_PUBLICATION_DATE, new Date()); + nodeService.setProperty(nodeRef, ContentModel.PROP_TITLE, "titleValue"); + recordsManagementActionService.executeRecordsManagementAction(nodeRef, "declareRecord"); + } + + return null; + } + }, false, true); + } + + protected void cutoff(final NodeRef ... nodeRefs) + { + retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.SYSTEM_USER_NAME); + + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.HOUR, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + + for (NodeRef nodeRef : nodeRefs) + { + NodeRef ndNodeRef = nodeService.getChildAssocs(nodeRef, RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + nodeService.setProperty(ndNodeRef, RecordsManagementModel.PROP_DISPOSITION_AS_OF, calendar.getTime()); + recordsManagementActionService.executeRecordsManagementAction(nodeRef, "cutoff", null); + } + + return null; + } + }, false, true); + } + + protected void makeEligible(final NodeRef ... nodeRefs) + { + retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.SYSTEM_USER_NAME); + + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.HOUR, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + + for (NodeRef nodeRef : nodeRefs) + { + NodeRef ndNodeRef = nodeService.getChildAssocs(nodeRef, RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + nodeService.setProperty(ndNodeRef, RecordsManagementModel.PROP_DISPOSITION_AS_OF, calendar.getTime()); + } + + return null; + } + }, false, true); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/CapabilitiesTest.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/CapabilitiesTest.java new file mode 100644 index 0000000000..ed97e266bd --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/CapabilitiesTest.java @@ -0,0 +1,3841 @@ +/* + * 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.module.org_alfresco_module_rm.test.capabilities; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.module.org_alfresco_module_rm.capability.Capability; +import org.alfresco.module.org_alfresco_module_rm.capability.RMPermissionModel; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.module.org_alfresco_module_rm.test.util.BaseRMTestCase; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.permissions.PermissionReference; +import org.alfresco.repo.security.permissions.impl.model.PermissionModel; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.AccessPermission; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.PermissionService; + +/** + * Test the RM permissions model + * + * @author Andy Hind + * @author Roy Wetherall + */ +public class CapabilitiesTest extends BaseRMTestCase implements + RMPermissionModel, RecordsManagementModel +{ + private NodeRef record; + + private PermissionModel permissionModel; + private PermissionService permissionService; + + @Override + protected void initServices() + { + super.initServices(); + + permissionModel = (PermissionModel) applicationContext.getBean("permissionsModelDAO"); + permissionService = (PermissionService) applicationContext.getBean("PermissionService"); + } + + @Override + protected boolean isUserTest() + { + return true; + } + + @Override + protected void setupTestDataImpl() + { + super.setupTestDataImpl(); + + record = createRecord(rmFolder, "CapabilitiesTest.txt"); + } + + @Override + protected void setupTestUsersImpl(NodeRef filePlan) + { + super.setupTestUsersImpl(filePlan); + + // Give all the users file permission objects + for (String user : testUsers) + { + securityService.setPermission(rmContainer, user, FILING); + } + } + + protected void check(Map access, String name, AccessStatus accessStatus) + { + Capability capability = securityService.getCapability(name); + assertNotNull(capability); + assertEquals(accessStatus, access.get(capability)); + } + + /** + * Check the RM permission model + */ + public void testPermissionsModel() + { + retryingTransactionHelper.doInTransaction( + new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + // As system user + AuthenticationUtil + .setFullyAuthenticatedUser(AuthenticationUtil + .getSystemUserName()); + + Set exposed = permissionModel + .getExposedPermissions(ASPECT_FILE_PLAN_COMPONENT); + assertEquals(6, exposed.size()); + assertTrue(exposed.contains(permissionModel + .getPermissionReference( + ASPECT_FILE_PLAN_COMPONENT, + ROLE_ADMINISTRATOR))); + + // Check all the permission are there + Set all = permissionModel + .getAllPermissions(ASPECT_FILE_PLAN_COMPONENT); + assertEquals(58 /* capbilities */* 2 + 5 /* roles */ + + (2 /* Read+File */* 2) + 1 /* Filing */, all + .size()); + + /* + * Check the granting for each permission. It is assumed + * that the ROLE_ADMINISTRATOR always has grant + * permission so is automatically checked. + */ + checkGranting(ACCESS_AUDIT, ROLE_RECORDS_MANAGER); + checkGranting(ADD_MODIFY_EVENT_DATES, + ROLE_RECORDS_MANAGER, ROLE_SECURITY_OFFICER, + ROLE_POWER_USER); + checkGranting(APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + ROLE_RECORDS_MANAGER); + checkGranting(ATTACH_RULES_TO_METADATA_PROPERTIES, + ROLE_RECORDS_MANAGER); + checkGranting(AUTHORIZE_ALL_TRANSFERS, + ROLE_RECORDS_MANAGER); + checkGranting(AUTHORIZE_NOMINATED_TRANSFERS, + ROLE_RECORDS_MANAGER); + checkGranting(CHANGE_OR_DELETE_REFERENCES, + ROLE_RECORDS_MANAGER); + checkGranting(CLOSE_FOLDERS, ROLE_RECORDS_MANAGER, + ROLE_SECURITY_OFFICER, ROLE_POWER_USER); + checkGranting(CREATE_AND_ASSOCIATE_SELECTION_LISTS, + ROLE_RECORDS_MANAGER); + checkGranting( + CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, + ROLE_RECORDS_MANAGER, ROLE_SECURITY_OFFICER); + checkGranting(CREATE_MODIFY_DESTROY_EVENTS, + ROLE_RECORDS_MANAGER); + checkGranting(CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, + ROLE_RECORDS_MANAGER); + checkGranting(CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, + ROLE_RECORDS_MANAGER); + checkGranting(CREATE_MODIFY_DESTROY_FOLDERS, + ROLE_RECORDS_MANAGER, ROLE_SECURITY_OFFICER, + ROLE_POWER_USER); + checkGranting(CREATE_MODIFY_DESTROY_RECORD_TYPES, + ROLE_RECORDS_MANAGER); + checkGranting(CREATE_MODIFY_DESTROY_REFERENCE_TYPES, + ROLE_RECORDS_MANAGER); + checkGranting(CREATE_MODIFY_DESTROY_ROLES, + ROLE_RECORDS_MANAGER); + checkGranting(CREATE_MODIFY_DESTROY_TIMEFRAMES, + ROLE_RECORDS_MANAGER); + checkGranting(CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, + ROLE_RECORDS_MANAGER); + checkGranting(CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, + ROLE_RECORDS_MANAGER); + checkGranting(CYCLE_VITAL_RECORDS, + ROLE_RECORDS_MANAGER, ROLE_SECURITY_OFFICER, + ROLE_POWER_USER); + checkGranting(DECLARE_AUDIT_AS_RECORD, + ROLE_RECORDS_MANAGER); + checkGranting(DECLARE_RECORDS, ROLE_RECORDS_MANAGER, + ROLE_SECURITY_OFFICER, ROLE_POWER_USER, + ROLE_USER); + checkGranting(DECLARE_RECORDS_IN_CLOSED_FOLDERS, + ROLE_RECORDS_MANAGER, ROLE_SECURITY_OFFICER, + ROLE_POWER_USER); + checkGranting(DELETE_AUDIT, ROLE_RECORDS_MANAGER); + checkGranting(DELETE_LINKS, ROLE_RECORDS_MANAGER); + checkGranting(DELETE_RECORDS, ROLE_RECORDS_MANAGER); + checkGranting(DESTROY_RECORDS, ROLE_RECORDS_MANAGER); + checkGranting( + DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, + ROLE_RECORDS_MANAGER); + checkGranting(DISPLAY_RIGHTS_REPORT, + ROLE_RECORDS_MANAGER); + checkGranting(EDIT_DECLARED_RECORD_METADATA, + ROLE_RECORDS_MANAGER); + checkGranting(EDIT_NON_RECORD_METADATA, + ROLE_RECORDS_MANAGER, ROLE_SECURITY_OFFICER, + ROLE_POWER_USER); + checkGranting(EDIT_RECORD_METADATA, + ROLE_RECORDS_MANAGER, ROLE_SECURITY_OFFICER, + ROLE_POWER_USER); + checkGranting(EDIT_SELECTION_LISTS, + ROLE_RECORDS_MANAGER); + checkGranting(ENABLE_DISABLE_AUDIT_BY_TYPES, + ROLE_RECORDS_MANAGER); + checkGranting(EXPORT_AUDIT, ROLE_RECORDS_MANAGER); + checkGranting(EXTEND_RETENTION_PERIOD_OR_FREEZE, + ROLE_RECORDS_MANAGER); + checkGranting(MAKE_OPTIONAL_PARAMETERS_MANDATORY, + ROLE_RECORDS_MANAGER); + checkGranting(MANAGE_ACCESS_CONTROLS); + checkGranting(MANAGE_ACCESS_RIGHTS, + ROLE_RECORDS_MANAGER); + checkGranting(MANUALLY_CHANGE_DISPOSITION_DATES, + ROLE_RECORDS_MANAGER); + checkGranting(MAP_CLASSIFICATION_GUIDE_METADATA, + ROLE_RECORDS_MANAGER); + checkGranting(MAP_EMAIL_METADATA, ROLE_RECORDS_MANAGER); + checkGranting(MOVE_RECORDS, ROLE_RECORDS_MANAGER); + checkGranting(PASSWORD_CONTROL, ROLE_RECORDS_MANAGER); + checkGranting(PLANNING_REVIEW_CYCLES, + ROLE_RECORDS_MANAGER, ROLE_SECURITY_OFFICER, + ROLE_POWER_USER); + checkGranting(RE_OPEN_FOLDERS, ROLE_RECORDS_MANAGER, + ROLE_SECURITY_OFFICER, ROLE_POWER_USER); + checkGranting(SELECT_AUDIT_METADATA, + ROLE_RECORDS_MANAGER); + checkGranting(TRIGGER_AN_EVENT, ROLE_RECORDS_MANAGER); + checkGranting(UNDECLARE_RECORDS, ROLE_RECORDS_MANAGER); + checkGranting(UNFREEZE, ROLE_RECORDS_MANAGER); + checkGranting(UPDATE_CLASSIFICATION_DATES, + ROLE_RECORDS_MANAGER, ROLE_SECURITY_OFFICER); + checkGranting(UPDATE_EXEMPTION_CATEGORIES, + ROLE_RECORDS_MANAGER, ROLE_SECURITY_OFFICER); + checkGranting(UPDATE_TRIGGER_DATES, + ROLE_RECORDS_MANAGER); + checkGranting(UPDATE_VITAL_RECORD_CYCLE_INFORMATION, + ROLE_RECORDS_MANAGER); + checkGranting(UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, + ROLE_RECORDS_MANAGER, ROLE_SECURITY_OFFICER); + checkGranting(VIEW_RECORDS, ROLE_RECORDS_MANAGER, + ROLE_SECURITY_OFFICER, ROLE_POWER_USER, + ROLE_USER); + checkGranting(VIEW_UPDATE_REASONS_FOR_FREEZE, + ROLE_RECORDS_MANAGER); + + return null; + } + }, false, true); + } + + /** + * Check that the roles passed have grant on the permission passed. + * + * @param permission + * permission + * @param roles + * grant roles + */ + private void checkGranting(String permission, String... roles) + { + Set granting = permissionModel + .getGrantingPermissions(permissionModel.getPermissionReference( + RecordsManagementModel.ASPECT_FILE_PLAN_COMPONENT, + permission)); + Set test = new HashSet(); + test.addAll(granting); + Set nonRM = new HashSet(); + for (PermissionReference pr : granting) + { + if (!pr.getQName().equals( + RecordsManagementModel.ASPECT_FILE_PLAN_COMPONENT)) + { + nonRM.add(pr); + } + } + test.removeAll(nonRM); + assertEquals(roles.length + 2, test.size()); + + assertTrue(test.contains(permissionModel.getPermissionReference( + RecordsManagementModel.ASPECT_FILE_PLAN_COMPONENT, + ROLE_ADMINISTRATOR))); + for (String role : roles) + { + assertTrue(test.contains(permissionModel.getPermissionReference( + RecordsManagementModel.ASPECT_FILE_PLAN_COMPONENT, role))); + } + + } + + /** + * Test the capability configuration + */ + public void testConfig() + { + retryingTransactionHelper.doInTransaction( + new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + // As system user + AuthenticationUtil + .setFullyAuthenticatedUser(AuthenticationUtil + .getSystemUserName()); + + assertEquals(6, securityService.getProtectedAspects() + .size()); + assertEquals(13, securityService + .getProtectedProperties().size()); + + // Test action wire up + testCapabilityActions(0, ACCESS_AUDIT); + testCapabilityActions(2, ADD_MODIFY_EVENT_DATES); + testCapabilityActions(2, + APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF); + testCapabilityActions(0, + ATTACH_RULES_TO_METADATA_PROPERTIES); + testCapabilityActions(2, AUTHORIZE_ALL_TRANSFERS); + testCapabilityActions(2, AUTHORIZE_NOMINATED_TRANSFERS); + testCapabilityActions(0, CHANGE_OR_DELETE_REFERENCES); + testCapabilityActions(1, CLOSE_FOLDERS); + testCapabilityActions(0, + CREATE_AND_ASSOCIATE_SELECTION_LISTS); + testCapabilityActions(0, + CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES); + testCapabilityActions(0, CREATE_MODIFY_DESTROY_EVENTS); + testCapabilityActions(0, + CREATE_MODIFY_DESTROY_FILEPLAN_METADATA); + testCapabilityActions(0, + CREATE_MODIFY_DESTROY_FILEPLAN_TYPES); + testCapabilityActions(0, CREATE_MODIFY_DESTROY_FOLDERS); + testCapabilityActions(0, + CREATE_MODIFY_DESTROY_RECORD_TYPES); + testCapabilityActions(0, + CREATE_MODIFY_DESTROY_REFERENCE_TYPES); + testCapabilityActions(0, CREATE_MODIFY_DESTROY_ROLES); + testCapabilityActions(0, + CREATE_MODIFY_DESTROY_TIMEFRAMES); + testCapabilityActions(0, + CREATE_MODIFY_DESTROY_USERS_AND_GROUPS); + testCapabilityActions(0, + CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS); + testCapabilityActions(1, CYCLE_VITAL_RECORDS); + testCapabilityActions(0, DECLARE_AUDIT_AS_RECORD); + testCapabilityActions(2, DECLARE_RECORDS); + testCapabilityActions(1, + DECLARE_RECORDS_IN_CLOSED_FOLDERS); + testCapabilityActions(0, DELETE_AUDIT); + testCapabilityActions(0, DELETE_LINKS); + testCapabilityActions(0, DELETE_RECORDS); + testCapabilityActions(0, DESTROY_RECORDS); + testCapabilityActions(1, + DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION); + testCapabilityActions(0, DISPLAY_RIGHTS_REPORT); + testCapabilityActions(0, EDIT_DECLARED_RECORD_METADATA); + testCapabilityActions(0, EDIT_NON_RECORD_METADATA); + testCapabilityActions(0, EDIT_RECORD_METADATA); + testCapabilityActions(0, EDIT_SELECTION_LISTS); + testCapabilityActions(0, ENABLE_DISABLE_AUDIT_BY_TYPES); + testCapabilityActions(0, EXPORT_AUDIT); + testCapabilityActions(1, + EXTEND_RETENTION_PERIOD_OR_FREEZE); + testCapabilityActions(1, FILE_RECORDS); + testCapabilityActions(0, + MAKE_OPTIONAL_PARAMETERS_MANDATORY); + testCapabilityActions(0, MANAGE_ACCESS_CONTROLS); + testCapabilityActions(0, MANAGE_ACCESS_RIGHTS); + testCapabilityActions(1, + MANUALLY_CHANGE_DISPOSITION_DATES); + testCapabilityActions(0, + MAP_CLASSIFICATION_GUIDE_METADATA); + testCapabilityActions(0, MAP_EMAIL_METADATA); + testCapabilityActions(0, MOVE_RECORDS); + testCapabilityActions(0, PASSWORD_CONTROL); + testCapabilityActions(1, PLANNING_REVIEW_CYCLES); + testCapabilityActions(1, RE_OPEN_FOLDERS); + testCapabilityActions(0, SELECT_AUDIT_METADATA); + testCapabilityActions(0, TRIGGER_AN_EVENT); + testCapabilityActions(1, UNDECLARE_RECORDS); + testCapabilityActions(2, UNFREEZE); + testCapabilityActions(0, UPDATE_CLASSIFICATION_DATES); + testCapabilityActions(0, UPDATE_EXEMPTION_CATEGORIES); + testCapabilityActions(0, UPDATE_TRIGGER_DATES); + testCapabilityActions(0, + UPDATE_VITAL_RECORD_CYCLE_INFORMATION); + testCapabilityActions(0, + UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS); + testCapabilityActions(0, VIEW_RECORDS); + testCapabilityActions(1, VIEW_UPDATE_REASONS_FOR_FREEZE); + + return null; + } + }, false, true); + } + + /** + * Test the capability actions + * + * @param count + * @param capability + */ + private void testCapabilityActions(int count, String capability) + { + assertEquals(count, securityService.getCapability(capability) + .getActionNames().size()); + } + + /** + * Test file plan as system + */ + public void testFilePlanAsSystem() + { + retryingTransactionHelper.doInTransaction( + new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + // As system user + AuthenticationUtil + .setFullyAuthenticatedUser(AuthenticationUtil + .getSystemUserName()); + + Map access = securityService + .getCapabilities(filePlan); + assertEquals(65, access.size()); + check(access, ACCESS_AUDIT, AccessStatus.ALLOWED); + check(access, ADD_MODIFY_EVENT_DATES, + AccessStatus.DENIED); + check(access, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + AccessStatus.DENIED); + check(access, ATTACH_RULES_TO_METADATA_PROPERTIES, + AccessStatus.ALLOWED); + check(access, AUTHORIZE_ALL_TRANSFERS, + AccessStatus.DENIED); + check(access, AUTHORIZE_NOMINATED_TRANSFERS, + AccessStatus.DENIED); + check(access, CHANGE_OR_DELETE_REFERENCES, + AccessStatus.UNDETERMINED); + check(access, CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, CREATE_AND_ASSOCIATE_SELECTION_LISTS, + AccessStatus.ALLOWED); + check(access, + CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_EVENTS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_FOLDERS, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_RECORD_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_REFERENCE_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_ROLES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_TIMEFRAMES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, + AccessStatus.DENIED); + check(access, CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_AUDIT_AS_RECORD, + AccessStatus.ALLOWED); + check(access, DECLARE_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_RECORDS_IN_CLOSED_FOLDERS, + AccessStatus.DENIED); + check(access, DELETE_AUDIT, AccessStatus.ALLOWED); + check(access, DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, DELETE_RECORDS, AccessStatus.DENIED); + check(access, DESTROY_RECORDS, AccessStatus.DENIED); + check(access, + DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, + AccessStatus.DENIED); + check(access, DISPLAY_RIGHTS_REPORT, + AccessStatus.ALLOWED); + check(access, EDIT_DECLARED_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_NON_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, EDIT_SELECTION_LISTS, + AccessStatus.ALLOWED); + check(access, ENABLE_DISABLE_AUDIT_BY_TYPES, + AccessStatus.ALLOWED); + check(access, EXPORT_AUDIT, AccessStatus.ALLOWED); + check(access, EXTEND_RETENTION_PERIOD_OR_FREEZE, + AccessStatus.DENIED); + check(access, MAKE_OPTIONAL_PARAMETERS_MANDATORY, + AccessStatus.ALLOWED); + check(access, MANAGE_ACCESS_CONTROLS, + AccessStatus.ALLOWED); + check(access, MANAGE_ACCESS_RIGHTS, + AccessStatus.ALLOWED); + check(access, MANUALLY_CHANGE_DISPOSITION_DATES, + AccessStatus.DENIED); + check(access, MAP_CLASSIFICATION_GUIDE_METADATA, + AccessStatus.ALLOWED); + check(access, MAP_EMAIL_METADATA, AccessStatus.ALLOWED); + check(access, MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, PASSWORD_CONTROL, AccessStatus.ALLOWED); + check(access, PLANNING_REVIEW_CYCLES, + AccessStatus.DENIED); + check(access, RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, SELECT_AUDIT_METADATA, + AccessStatus.ALLOWED); + check(access, TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, UNFREEZE, AccessStatus.DENIED); + check(access, UPDATE_CLASSIFICATION_DATES, + AccessStatus.ALLOWED); + check(access, UPDATE_EXEMPTION_CATEGORIES, + AccessStatus.ALLOWED); + check(access, UPDATE_TRIGGER_DATES, + AccessStatus.ALLOWED); + check(access, UPDATE_VITAL_RECORD_CYCLE_INFORMATION, + AccessStatus.ALLOWED); + check(access, UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, + AccessStatus.ALLOWED); + check(access, VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, VIEW_UPDATE_REASONS_FOR_FREEZE, + AccessStatus.DENIED); + + return null; + } + }, false, true); + } + + public void testFilePlanAsAdmin() + { + retryingTransactionHelper.doInTransaction( + new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil + .setFullyAuthenticatedUser(AuthenticationUtil + .getAdminUserName()); + Map access = securityService + .getCapabilities(filePlan); + assertEquals(65, access.size()); + check(access, ACCESS_AUDIT, AccessStatus.ALLOWED); + check(access, ADD_MODIFY_EVENT_DATES, + AccessStatus.DENIED); + check(access, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + AccessStatus.DENIED); + check(access, ATTACH_RULES_TO_METADATA_PROPERTIES, + AccessStatus.ALLOWED); + check(access, AUTHORIZE_ALL_TRANSFERS, + AccessStatus.DENIED); + check(access, AUTHORIZE_NOMINATED_TRANSFERS, + AccessStatus.DENIED); + check(access, CHANGE_OR_DELETE_REFERENCES, + AccessStatus.UNDETERMINED); + check(access, CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, CREATE_AND_ASSOCIATE_SELECTION_LISTS, + AccessStatus.ALLOWED); + check(access, + CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_EVENTS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_FOLDERS, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_RECORD_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_REFERENCE_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_ROLES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_TIMEFRAMES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, + AccessStatus.DENIED); + check(access, CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_AUDIT_AS_RECORD, + AccessStatus.ALLOWED); + check(access, DECLARE_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_RECORDS_IN_CLOSED_FOLDERS, + AccessStatus.DENIED); + check(access, DELETE_AUDIT, AccessStatus.ALLOWED); + check(access, DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, DELETE_RECORDS, AccessStatus.DENIED); + check(access, DESTROY_RECORDS, AccessStatus.DENIED); + check(access, + DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, + AccessStatus.DENIED); + check(access, DISPLAY_RIGHTS_REPORT, + AccessStatus.ALLOWED); + check(access, EDIT_DECLARED_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_NON_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, EDIT_SELECTION_LISTS, + AccessStatus.ALLOWED); + check(access, ENABLE_DISABLE_AUDIT_BY_TYPES, + AccessStatus.ALLOWED); + check(access, EXPORT_AUDIT, AccessStatus.ALLOWED); + check(access, EXTEND_RETENTION_PERIOD_OR_FREEZE, + AccessStatus.DENIED); + check(access, MAKE_OPTIONAL_PARAMETERS_MANDATORY, + AccessStatus.ALLOWED); + check(access, MANAGE_ACCESS_CONTROLS, + AccessStatus.ALLOWED); + check(access, MANAGE_ACCESS_RIGHTS, + AccessStatus.ALLOWED); + check(access, MANUALLY_CHANGE_DISPOSITION_DATES, + AccessStatus.DENIED); + check(access, MAP_CLASSIFICATION_GUIDE_METADATA, + AccessStatus.ALLOWED); + check(access, MAP_EMAIL_METADATA, AccessStatus.ALLOWED); + check(access, MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, PASSWORD_CONTROL, AccessStatus.ALLOWED); + check(access, PLANNING_REVIEW_CYCLES, + AccessStatus.DENIED); + check(access, RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, SELECT_AUDIT_METADATA, + AccessStatus.ALLOWED); + check(access, TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, UNFREEZE, AccessStatus.DENIED); + check(access, UPDATE_CLASSIFICATION_DATES, + AccessStatus.ALLOWED); + check(access, UPDATE_EXEMPTION_CATEGORIES, + AccessStatus.ALLOWED); + check(access, UPDATE_TRIGGER_DATES, + AccessStatus.ALLOWED); + check(access, UPDATE_VITAL_RECORD_CYCLE_INFORMATION, + AccessStatus.ALLOWED); + check(access, UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, + AccessStatus.ALLOWED); + check(access, VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, VIEW_UPDATE_REASONS_FOR_FREEZE, + AccessStatus.DENIED); + + return null; + } + }, false, true); + } + + /** + * Test file plan as administrator + */ + public void testFilePlanAsAdministrator() + { + retryingTransactionHelper.doInTransaction( + new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil + .setFullyAuthenticatedUser(rmAdminName); + Map access = securityService + .getCapabilities(filePlan); + assertEquals(65, access.size()); + check(access, ACCESS_AUDIT, AccessStatus.ALLOWED); + check(access, ADD_MODIFY_EVENT_DATES, + AccessStatus.DENIED); + check(access, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + AccessStatus.DENIED); + check(access, ATTACH_RULES_TO_METADATA_PROPERTIES, + AccessStatus.ALLOWED); + check(access, AUTHORIZE_ALL_TRANSFERS, + AccessStatus.DENIED); + check(access, AUTHORIZE_NOMINATED_TRANSFERS, + AccessStatus.DENIED); + check(access, CHANGE_OR_DELETE_REFERENCES, + AccessStatus.UNDETERMINED); + check(access, CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, CREATE_AND_ASSOCIATE_SELECTION_LISTS, + AccessStatus.ALLOWED); + check(access, + CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_EVENTS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_FOLDERS, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_RECORD_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_REFERENCE_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_ROLES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_TIMEFRAMES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, + AccessStatus.DENIED); + check(access, CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_AUDIT_AS_RECORD, + AccessStatus.ALLOWED); + check(access, DECLARE_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_RECORDS_IN_CLOSED_FOLDERS, + AccessStatus.DENIED); + check(access, DELETE_AUDIT, AccessStatus.ALLOWED); + check(access, DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, DELETE_RECORDS, AccessStatus.DENIED); + check(access, DESTROY_RECORDS, AccessStatus.DENIED); + check(access, + DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, + AccessStatus.DENIED); + check(access, DISPLAY_RIGHTS_REPORT, + AccessStatus.ALLOWED); + check(access, EDIT_DECLARED_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_NON_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, EDIT_SELECTION_LISTS, + AccessStatus.ALLOWED); + check(access, ENABLE_DISABLE_AUDIT_BY_TYPES, + AccessStatus.ALLOWED); + check(access, EXPORT_AUDIT, AccessStatus.ALLOWED); + check(access, EXTEND_RETENTION_PERIOD_OR_FREEZE, + AccessStatus.DENIED); + check(access, MAKE_OPTIONAL_PARAMETERS_MANDATORY, + AccessStatus.ALLOWED); + check(access, MANAGE_ACCESS_CONTROLS, + AccessStatus.ALLOWED); + check(access, MANAGE_ACCESS_RIGHTS, + AccessStatus.ALLOWED); + check(access, MANUALLY_CHANGE_DISPOSITION_DATES, + AccessStatus.DENIED); + check(access, MAP_CLASSIFICATION_GUIDE_METADATA, + AccessStatus.ALLOWED); + check(access, MAP_EMAIL_METADATA, AccessStatus.ALLOWED); + check(access, MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, PASSWORD_CONTROL, AccessStatus.ALLOWED); + check(access, PLANNING_REVIEW_CYCLES, + AccessStatus.DENIED); + check(access, RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, SELECT_AUDIT_METADATA, + AccessStatus.ALLOWED); + check(access, TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, UNFREEZE, AccessStatus.DENIED); + check(access, UPDATE_CLASSIFICATION_DATES, + AccessStatus.ALLOWED); + check(access, UPDATE_EXEMPTION_CATEGORIES, + AccessStatus.ALLOWED); + check(access, UPDATE_TRIGGER_DATES, + AccessStatus.ALLOWED); + check(access, UPDATE_VITAL_RECORD_CYCLE_INFORMATION, + AccessStatus.ALLOWED); + check(access, UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, + AccessStatus.ALLOWED); + check(access, VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, VIEW_UPDATE_REASONS_FOR_FREEZE, + AccessStatus.DENIED); + + return null; + } + }, false, true); + } + + public void testFilePlanAsRecordsManager() + { + retryingTransactionHelper.doInTransaction( + new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + Set permissions = permissionService + .getAllSetPermissions(filePlan); + for (AccessPermission ap : permissions) + { + System.out.println(ap.getAuthority() + " -> " + + ap.getPermission() + " (" + + ap.getPosition() + ")"); + } + + AuthenticationUtil + .setFullyAuthenticatedUser(recordsManagerName); + Map access = securityService + .getCapabilities(filePlan); + assertEquals(65, access.size()); // 58 + File + check(access, ACCESS_AUDIT, AccessStatus.ALLOWED); + check(access, ADD_MODIFY_EVENT_DATES, + AccessStatus.DENIED); + check(access, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + AccessStatus.DENIED); + check(access, ATTACH_RULES_TO_METADATA_PROPERTIES, + AccessStatus.ALLOWED); + check(access, AUTHORIZE_ALL_TRANSFERS, + AccessStatus.DENIED); + check(access, AUTHORIZE_NOMINATED_TRANSFERS, + AccessStatus.DENIED); + check(access, CHANGE_OR_DELETE_REFERENCES, + AccessStatus.UNDETERMINED); + check(access, CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, CREATE_AND_ASSOCIATE_SELECTION_LISTS, + AccessStatus.ALLOWED); + check(access, + CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_EVENTS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_FOLDERS, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_RECORD_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_REFERENCE_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_ROLES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_TIMEFRAMES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, + AccessStatus.DENIED); + check(access, CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_AUDIT_AS_RECORD, + AccessStatus.ALLOWED); + check(access, DECLARE_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_RECORDS_IN_CLOSED_FOLDERS, + AccessStatus.DENIED); + check(access, DELETE_AUDIT, AccessStatus.ALLOWED); + check(access, DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, DELETE_RECORDS, AccessStatus.DENIED); + check(access, DESTROY_RECORDS, AccessStatus.DENIED); + check(access, + DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, + AccessStatus.DENIED); + check(access, DISPLAY_RIGHTS_REPORT, + AccessStatus.ALLOWED); + check(access, EDIT_DECLARED_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_NON_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, EDIT_SELECTION_LISTS, + AccessStatus.ALLOWED); + check(access, ENABLE_DISABLE_AUDIT_BY_TYPES, + AccessStatus.ALLOWED); + check(access, EXPORT_AUDIT, AccessStatus.ALLOWED); + check(access, EXTEND_RETENTION_PERIOD_OR_FREEZE, + AccessStatus.DENIED); + check(access, MAKE_OPTIONAL_PARAMETERS_MANDATORY, + AccessStatus.ALLOWED); + check(access, MANAGE_ACCESS_CONTROLS, + AccessStatus.DENIED); + check(access, MANAGE_ACCESS_RIGHTS, + AccessStatus.ALLOWED); + check(access, MANUALLY_CHANGE_DISPOSITION_DATES, + AccessStatus.DENIED); + check(access, MAP_CLASSIFICATION_GUIDE_METADATA, + AccessStatus.ALLOWED); + check(access, MAP_EMAIL_METADATA, AccessStatus.ALLOWED); + check(access, MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, PASSWORD_CONTROL, AccessStatus.ALLOWED); + check(access, PLANNING_REVIEW_CYCLES, + AccessStatus.DENIED); + check(access, RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, SELECT_AUDIT_METADATA, + AccessStatus.ALLOWED); + check(access, TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, UNFREEZE, AccessStatus.DENIED); + check(access, UPDATE_CLASSIFICATION_DATES, + AccessStatus.ALLOWED); + check(access, UPDATE_EXEMPTION_CATEGORIES, + AccessStatus.ALLOWED); + check(access, UPDATE_TRIGGER_DATES, + AccessStatus.ALLOWED); + check(access, UPDATE_VITAL_RECORD_CYCLE_INFORMATION, + AccessStatus.ALLOWED); + check(access, UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, + AccessStatus.ALLOWED); + check(access, VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, VIEW_UPDATE_REASONS_FOR_FREEZE, + AccessStatus.DENIED); + + return null; + } + }, false, true); + } + + public void testFilePlanAsSecurityOfficer() + { + retryingTransactionHelper.doInTransaction( + new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + List temp = new ArrayList(); + temp.add("ACCESS_AUDIT"); + capabilityService.getCapabilitiesAccessState(filePlan, temp); + + + AuthenticationUtil + .setFullyAuthenticatedUser(securityOfficerName); + Map access = securityService + .getCapabilities(filePlan); + assertEquals(65, access.size()); // 58 + File + check(access, ACCESS_AUDIT, AccessStatus.DENIED); + check(access, ADD_MODIFY_EVENT_DATES, + AccessStatus.DENIED); + check(access, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + AccessStatus.DENIED); + check(access, ATTACH_RULES_TO_METADATA_PROPERTIES, + AccessStatus.DENIED); + check(access, AUTHORIZE_ALL_TRANSFERS, + AccessStatus.DENIED); + check(access, AUTHORIZE_NOMINATED_TRANSFERS, + AccessStatus.DENIED); + check(access, CHANGE_OR_DELETE_REFERENCES, + AccessStatus.DENIED); + check(access, CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, CREATE_AND_ASSOCIATE_SELECTION_LISTS, + AccessStatus.DENIED); + check(access, + CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_EVENTS, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FOLDERS, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_RECORD_TYPES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_REFERENCE_TYPES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_ROLES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_TIMEFRAMES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, + AccessStatus.DENIED); + check(access, CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_AUDIT_AS_RECORD, + AccessStatus.DENIED); + check(access, DECLARE_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_RECORDS_IN_CLOSED_FOLDERS, + AccessStatus.DENIED); + check(access, DELETE_AUDIT, AccessStatus.DENIED); + check(access, DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, DELETE_RECORDS, AccessStatus.DENIED); + check(access, DESTROY_RECORDS, AccessStatus.DENIED); + check(access, + DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, + AccessStatus.DENIED); + check(access, DISPLAY_RIGHTS_REPORT, + AccessStatus.DENIED); + check(access, EDIT_DECLARED_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_NON_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, EDIT_SELECTION_LISTS, AccessStatus.DENIED); + check(access, ENABLE_DISABLE_AUDIT_BY_TYPES, + AccessStatus.DENIED); + check(access, EXPORT_AUDIT, AccessStatus.DENIED); + check(access, EXTEND_RETENTION_PERIOD_OR_FREEZE, + AccessStatus.DENIED); + check(access, MAKE_OPTIONAL_PARAMETERS_MANDATORY, + AccessStatus.DENIED); + check(access, MANAGE_ACCESS_CONTROLS, + AccessStatus.DENIED); + check(access, MANAGE_ACCESS_RIGHTS, AccessStatus.DENIED); + check(access, MANUALLY_CHANGE_DISPOSITION_DATES, + AccessStatus.DENIED); + check(access, MAP_CLASSIFICATION_GUIDE_METADATA, + AccessStatus.DENIED); + check(access, MAP_EMAIL_METADATA, AccessStatus.DENIED); + check(access, MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, PASSWORD_CONTROL, AccessStatus.DENIED); + check(access, PLANNING_REVIEW_CYCLES, + AccessStatus.DENIED); + check(access, RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, SELECT_AUDIT_METADATA, + AccessStatus.DENIED); + check(access, TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, UNFREEZE, AccessStatus.DENIED); + check(access, UPDATE_CLASSIFICATION_DATES, + AccessStatus.ALLOWED); + check(access, UPDATE_EXEMPTION_CATEGORIES, + AccessStatus.ALLOWED); + check(access, UPDATE_TRIGGER_DATES, AccessStatus.DENIED); + check(access, UPDATE_VITAL_RECORD_CYCLE_INFORMATION, + AccessStatus.DENIED); + check(access, UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, + AccessStatus.ALLOWED); + check(access, VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, VIEW_UPDATE_REASONS_FOR_FREEZE, + AccessStatus.DENIED); + + return null; + } + }, false, true); + } + + /** + * Test file plan as power user + */ + public void testFilePlanAsPowerUser() + { + retryingTransactionHelper.doInTransaction( + new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil + .setFullyAuthenticatedUser(powerUserName); + Map access = securityService + .getCapabilities(filePlan); + assertEquals(65, access.size()); // 58 + File + check(access, ACCESS_AUDIT, AccessStatus.DENIED); + check(access, ADD_MODIFY_EVENT_DATES, + AccessStatus.DENIED); + check(access, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + AccessStatus.DENIED); + check(access, ATTACH_RULES_TO_METADATA_PROPERTIES, + AccessStatus.DENIED); + check(access, AUTHORIZE_ALL_TRANSFERS, + AccessStatus.DENIED); + check(access, AUTHORIZE_NOMINATED_TRANSFERS, + AccessStatus.DENIED); + check(access, CHANGE_OR_DELETE_REFERENCES, + AccessStatus.DENIED); + check(access, CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, CREATE_AND_ASSOCIATE_SELECTION_LISTS, + AccessStatus.DENIED); + check(access, + CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_EVENTS, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FOLDERS, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_RECORD_TYPES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_REFERENCE_TYPES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_ROLES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_TIMEFRAMES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, + AccessStatus.DENIED); + check(access, CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_AUDIT_AS_RECORD, + AccessStatus.DENIED); + check(access, DECLARE_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_RECORDS_IN_CLOSED_FOLDERS, + AccessStatus.DENIED); + check(access, DELETE_AUDIT, AccessStatus.DENIED); + check(access, DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, DELETE_RECORDS, AccessStatus.DENIED); + check(access, DESTROY_RECORDS, AccessStatus.DENIED); + check(access, + DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, + AccessStatus.DENIED); + check(access, DISPLAY_RIGHTS_REPORT, + AccessStatus.DENIED); + check(access, EDIT_DECLARED_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_NON_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, EDIT_SELECTION_LISTS, AccessStatus.DENIED); + check(access, ENABLE_DISABLE_AUDIT_BY_TYPES, + AccessStatus.DENIED); + check(access, EXPORT_AUDIT, AccessStatus.DENIED); + check(access, EXTEND_RETENTION_PERIOD_OR_FREEZE, + AccessStatus.DENIED); + check(access, MAKE_OPTIONAL_PARAMETERS_MANDATORY, + AccessStatus.DENIED); + check(access, MANAGE_ACCESS_CONTROLS, + AccessStatus.DENIED); + check(access, MANAGE_ACCESS_RIGHTS, AccessStatus.DENIED); + check(access, MANUALLY_CHANGE_DISPOSITION_DATES, + AccessStatus.DENIED); + check(access, MAP_CLASSIFICATION_GUIDE_METADATA, + AccessStatus.DENIED); + check(access, MAP_EMAIL_METADATA, AccessStatus.DENIED); + check(access, MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, PASSWORD_CONTROL, AccessStatus.DENIED); + check(access, PLANNING_REVIEW_CYCLES, + AccessStatus.DENIED); + check(access, RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, SELECT_AUDIT_METADATA, + AccessStatus.DENIED); + check(access, TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, UNFREEZE, AccessStatus.DENIED); + check(access, UPDATE_CLASSIFICATION_DATES, + AccessStatus.DENIED); + check(access, UPDATE_EXEMPTION_CATEGORIES, + AccessStatus.DENIED); + check(access, UPDATE_TRIGGER_DATES, AccessStatus.DENIED); + check(access, UPDATE_VITAL_RECORD_CYCLE_INFORMATION, + AccessStatus.DENIED); + check(access, UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, + AccessStatus.DENIED); + check(access, VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, VIEW_UPDATE_REASONS_FOR_FREEZE, + AccessStatus.DENIED); + + return null; + } + }, false, true); + } + + /** + * Test file plan as user + */ + public void testFilePlanAsUser() + { + retryingTransactionHelper.doInTransaction( + new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil + .setFullyAuthenticatedUser(rmUserName); + Map access = securityService + .getCapabilities(filePlan); + assertEquals(65, access.size()); // 58 + File + check(access, ACCESS_AUDIT, AccessStatus.DENIED); + check(access, ADD_MODIFY_EVENT_DATES, + AccessStatus.DENIED); + check(access, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + AccessStatus.DENIED); + check(access, ATTACH_RULES_TO_METADATA_PROPERTIES, + AccessStatus.DENIED); + check(access, AUTHORIZE_ALL_TRANSFERS, + AccessStatus.DENIED); + check(access, AUTHORIZE_NOMINATED_TRANSFERS, + AccessStatus.DENIED); + check(access, CHANGE_OR_DELETE_REFERENCES, + AccessStatus.DENIED); + check(access, CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, CREATE_AND_ASSOCIATE_SELECTION_LISTS, + AccessStatus.DENIED); + check(access, + CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_EVENTS, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FOLDERS, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_RECORD_TYPES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_REFERENCE_TYPES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_ROLES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_TIMEFRAMES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, + AccessStatus.DENIED); + check(access, CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_AUDIT_AS_RECORD, + AccessStatus.DENIED); + check(access, DECLARE_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_RECORDS_IN_CLOSED_FOLDERS, + AccessStatus.DENIED); + check(access, DELETE_AUDIT, AccessStatus.DENIED); + check(access, DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, DELETE_RECORDS, AccessStatus.DENIED); + check(access, DESTROY_RECORDS, AccessStatus.DENIED); + check(access, + DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, + AccessStatus.DENIED); + check(access, DISPLAY_RIGHTS_REPORT, + AccessStatus.DENIED); + check(access, EDIT_DECLARED_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_NON_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, EDIT_SELECTION_LISTS, AccessStatus.DENIED); + check(access, ENABLE_DISABLE_AUDIT_BY_TYPES, + AccessStatus.DENIED); + check(access, EXPORT_AUDIT, AccessStatus.DENIED); + check(access, EXTEND_RETENTION_PERIOD_OR_FREEZE, + AccessStatus.DENIED); + check(access, MAKE_OPTIONAL_PARAMETERS_MANDATORY, + AccessStatus.DENIED); + check(access, MANAGE_ACCESS_CONTROLS, + AccessStatus.DENIED); + check(access, MANAGE_ACCESS_RIGHTS, AccessStatus.DENIED); + check(access, MANUALLY_CHANGE_DISPOSITION_DATES, + AccessStatus.DENIED); + check(access, MAP_CLASSIFICATION_GUIDE_METADATA, + AccessStatus.DENIED); + check(access, MAP_EMAIL_METADATA, AccessStatus.DENIED); + check(access, MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, PASSWORD_CONTROL, AccessStatus.DENIED); + check(access, PLANNING_REVIEW_CYCLES, + AccessStatus.DENIED); + check(access, RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, SELECT_AUDIT_METADATA, + AccessStatus.DENIED); + check(access, TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, UNFREEZE, AccessStatus.DENIED); + check(access, UPDATE_CLASSIFICATION_DATES, + AccessStatus.DENIED); + check(access, UPDATE_EXEMPTION_CATEGORIES, + AccessStatus.DENIED); + check(access, UPDATE_TRIGGER_DATES, AccessStatus.DENIED); + check(access, UPDATE_VITAL_RECORD_CYCLE_INFORMATION, + AccessStatus.DENIED); + check(access, UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, + AccessStatus.DENIED); + check(access, VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, VIEW_UPDATE_REASONS_FOR_FREEZE, + AccessStatus.DENIED); + + return null; + } + }, false, true); + } + + /** + * Test record category as system + */ + public void testRecordCategoryAsSystem() + { + retryingTransactionHelper.doInTransaction( + new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil + .setFullyAuthenticatedUser(AuthenticationUtil.SYSTEM_USER_NAME); + + Map access = securityService + .getCapabilities(rmContainer); + assertEquals(65, access.size()); + check(access, ACCESS_AUDIT, AccessStatus.ALLOWED); + check(access, ADD_MODIFY_EVENT_DATES, + AccessStatus.DENIED); + check(access, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + AccessStatus.DENIED); + check(access, ATTACH_RULES_TO_METADATA_PROPERTIES, + AccessStatus.ALLOWED); + check(access, AUTHORIZE_ALL_TRANSFERS, + AccessStatus.DENIED); + check(access, AUTHORIZE_NOMINATED_TRANSFERS, + AccessStatus.DENIED); + check(access, CHANGE_OR_DELETE_REFERENCES, + AccessStatus.UNDETERMINED); + check(access, CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, CREATE_AND_ASSOCIATE_SELECTION_LISTS, + AccessStatus.ALLOWED); + check(access, + CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_EVENTS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_FOLDERS, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_RECORD_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_REFERENCE_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_ROLES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_TIMEFRAMES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, + AccessStatus.DENIED); + check(access, CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_AUDIT_AS_RECORD, + AccessStatus.ALLOWED); + check(access, DECLARE_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_RECORDS_IN_CLOSED_FOLDERS, + AccessStatus.DENIED); + check(access, DELETE_AUDIT, AccessStatus.ALLOWED); + check(access, DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, DELETE_RECORDS, AccessStatus.DENIED); + check(access, DESTROY_RECORDS, AccessStatus.DENIED); + check(access, + DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, + AccessStatus.DENIED); + check(access, DISPLAY_RIGHTS_REPORT, + AccessStatus.ALLOWED); + check(access, EDIT_DECLARED_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_NON_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, EDIT_SELECTION_LISTS, + AccessStatus.ALLOWED); + check(access, ENABLE_DISABLE_AUDIT_BY_TYPES, + AccessStatus.ALLOWED); + check(access, EXPORT_AUDIT, AccessStatus.ALLOWED); + check(access, EXTEND_RETENTION_PERIOD_OR_FREEZE, + AccessStatus.DENIED); + check(access, MAKE_OPTIONAL_PARAMETERS_MANDATORY, + AccessStatus.ALLOWED); + check(access, MANAGE_ACCESS_CONTROLS, + AccessStatus.ALLOWED); + check(access, MANAGE_ACCESS_RIGHTS, + AccessStatus.ALLOWED); + check(access, MANUALLY_CHANGE_DISPOSITION_DATES, + AccessStatus.DENIED); + check(access, MAP_CLASSIFICATION_GUIDE_METADATA, + AccessStatus.ALLOWED); + check(access, MAP_EMAIL_METADATA, AccessStatus.ALLOWED); + check(access, MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, PASSWORD_CONTROL, AccessStatus.ALLOWED); + check(access, PLANNING_REVIEW_CYCLES, + AccessStatus.DENIED); + check(access, RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, SELECT_AUDIT_METADATA, + AccessStatus.ALLOWED); + check(access, TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, UNFREEZE, AccessStatus.DENIED); + check(access, UPDATE_CLASSIFICATION_DATES, + AccessStatus.ALLOWED); + check(access, UPDATE_EXEMPTION_CATEGORIES, + AccessStatus.ALLOWED); + check(access, UPDATE_TRIGGER_DATES, + AccessStatus.ALLOWED); + check(access, UPDATE_VITAL_RECORD_CYCLE_INFORMATION, + AccessStatus.ALLOWED); + check(access, UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, + AccessStatus.ALLOWED); + check(access, VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, VIEW_UPDATE_REASONS_FOR_FREEZE, + AccessStatus.DENIED); + + return null; + } + }, false, true); + } + + /** + * Test record category as admin + */ + public void testRecordCategoryAsAdmin() + { + retryingTransactionHelper.doInTransaction( + new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil + .setFullyAuthenticatedUser(AuthenticationUtil + .getAdminUserName()); + Map access = securityService + .getCapabilities(rmContainer); + assertEquals(65, access.size()); + check(access, ACCESS_AUDIT, AccessStatus.ALLOWED); + check(access, ADD_MODIFY_EVENT_DATES, + AccessStatus.DENIED); + check(access, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + AccessStatus.DENIED); + check(access, ATTACH_RULES_TO_METADATA_PROPERTIES, + AccessStatus.ALLOWED); + check(access, AUTHORIZE_ALL_TRANSFERS, + AccessStatus.DENIED); + check(access, AUTHORIZE_NOMINATED_TRANSFERS, + AccessStatus.DENIED); + check(access, CHANGE_OR_DELETE_REFERENCES, + AccessStatus.UNDETERMINED); + check(access, CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, CREATE_AND_ASSOCIATE_SELECTION_LISTS, + AccessStatus.ALLOWED); + check(access, + CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_EVENTS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_FOLDERS, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_RECORD_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_REFERENCE_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_ROLES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_TIMEFRAMES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, + AccessStatus.DENIED); + check(access, CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_AUDIT_AS_RECORD, + AccessStatus.ALLOWED); + check(access, DECLARE_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_RECORDS_IN_CLOSED_FOLDERS, + AccessStatus.DENIED); + check(access, DELETE_AUDIT, AccessStatus.ALLOWED); + check(access, DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, DELETE_RECORDS, AccessStatus.DENIED); + check(access, DESTROY_RECORDS, AccessStatus.DENIED); + check(access, + DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, + AccessStatus.DENIED); + check(access, DISPLAY_RIGHTS_REPORT, + AccessStatus.ALLOWED); + check(access, EDIT_DECLARED_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_NON_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, EDIT_SELECTION_LISTS, + AccessStatus.ALLOWED); + check(access, ENABLE_DISABLE_AUDIT_BY_TYPES, + AccessStatus.ALLOWED); + check(access, EXPORT_AUDIT, AccessStatus.ALLOWED); + check(access, EXTEND_RETENTION_PERIOD_OR_FREEZE, + AccessStatus.DENIED); + check(access, MAKE_OPTIONAL_PARAMETERS_MANDATORY, + AccessStatus.ALLOWED); + check(access, MANAGE_ACCESS_CONTROLS, + AccessStatus.ALLOWED); + check(access, MANAGE_ACCESS_RIGHTS, + AccessStatus.ALLOWED); + check(access, MANUALLY_CHANGE_DISPOSITION_DATES, + AccessStatus.DENIED); + check(access, MAP_CLASSIFICATION_GUIDE_METADATA, + AccessStatus.ALLOWED); + check(access, MAP_EMAIL_METADATA, AccessStatus.ALLOWED); + check(access, MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, PASSWORD_CONTROL, AccessStatus.ALLOWED); + check(access, PLANNING_REVIEW_CYCLES, + AccessStatus.DENIED); + check(access, RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, SELECT_AUDIT_METADATA, + AccessStatus.ALLOWED); + check(access, TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, UNFREEZE, AccessStatus.DENIED); + check(access, UPDATE_CLASSIFICATION_DATES, + AccessStatus.ALLOWED); + check(access, UPDATE_EXEMPTION_CATEGORIES, + AccessStatus.ALLOWED); + check(access, UPDATE_TRIGGER_DATES, + AccessStatus.ALLOWED); + check(access, UPDATE_VITAL_RECORD_CYCLE_INFORMATION, + AccessStatus.ALLOWED); + check(access, UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, + AccessStatus.ALLOWED); + check(access, VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, VIEW_UPDATE_REASONS_FOR_FREEZE, + AccessStatus.DENIED); + + return null; + } + }, false, true); + } + + /** + * Test record category as administrator + */ + public void testRecordCategoryAsAdministrator() + { + retryingTransactionHelper.doInTransaction( + new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil + .setFullyAuthenticatedUser(rmAdminName); + Map access = securityService + .getCapabilities(rmContainer); + assertEquals(65, access.size()); + check(access, ACCESS_AUDIT, AccessStatus.ALLOWED); + check(access, ADD_MODIFY_EVENT_DATES, + AccessStatus.DENIED); + check(access, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + AccessStatus.DENIED); + check(access, ATTACH_RULES_TO_METADATA_PROPERTIES, + AccessStatus.ALLOWED); + check(access, AUTHORIZE_ALL_TRANSFERS, + AccessStatus.DENIED); + check(access, AUTHORIZE_NOMINATED_TRANSFERS, + AccessStatus.DENIED); + check(access, CHANGE_OR_DELETE_REFERENCES, + AccessStatus.UNDETERMINED); + check(access, CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, CREATE_AND_ASSOCIATE_SELECTION_LISTS, + AccessStatus.ALLOWED); + check(access, + CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_EVENTS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_FOLDERS, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_RECORD_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_REFERENCE_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_ROLES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_TIMEFRAMES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, + AccessStatus.DENIED); + check(access, CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_AUDIT_AS_RECORD, + AccessStatus.ALLOWED); + check(access, DECLARE_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_RECORDS_IN_CLOSED_FOLDERS, + AccessStatus.DENIED); + check(access, DELETE_AUDIT, AccessStatus.ALLOWED); + check(access, DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, DELETE_RECORDS, AccessStatus.DENIED); + check(access, DESTROY_RECORDS, AccessStatus.DENIED); + check(access, + DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, + AccessStatus.DENIED); + check(access, DISPLAY_RIGHTS_REPORT, + AccessStatus.ALLOWED); + check(access, EDIT_DECLARED_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_NON_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, EDIT_SELECTION_LISTS, + AccessStatus.ALLOWED); + check(access, ENABLE_DISABLE_AUDIT_BY_TYPES, + AccessStatus.ALLOWED); + check(access, EXPORT_AUDIT, AccessStatus.ALLOWED); + check(access, EXTEND_RETENTION_PERIOD_OR_FREEZE, + AccessStatus.DENIED); + check(access, MAKE_OPTIONAL_PARAMETERS_MANDATORY, + AccessStatus.ALLOWED); + check(access, MANAGE_ACCESS_CONTROLS, + AccessStatus.ALLOWED); + check(access, MANAGE_ACCESS_RIGHTS, + AccessStatus.ALLOWED); + check(access, MANUALLY_CHANGE_DISPOSITION_DATES, + AccessStatus.DENIED); + check(access, MAP_CLASSIFICATION_GUIDE_METADATA, + AccessStatus.ALLOWED); + check(access, MAP_EMAIL_METADATA, AccessStatus.ALLOWED); + check(access, MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, PASSWORD_CONTROL, AccessStatus.ALLOWED); + check(access, PLANNING_REVIEW_CYCLES, + AccessStatus.DENIED); + check(access, RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, SELECT_AUDIT_METADATA, + AccessStatus.ALLOWED); + check(access, TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, UNFREEZE, AccessStatus.DENIED); + check(access, UPDATE_CLASSIFICATION_DATES, + AccessStatus.ALLOWED); + check(access, UPDATE_EXEMPTION_CATEGORIES, + AccessStatus.ALLOWED); + check(access, UPDATE_TRIGGER_DATES, + AccessStatus.ALLOWED); + check(access, UPDATE_VITAL_RECORD_CYCLE_INFORMATION, + AccessStatus.ALLOWED); + check(access, UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, + AccessStatus.ALLOWED); + check(access, VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, VIEW_UPDATE_REASONS_FOR_FREEZE, + AccessStatus.DENIED); + + return null; + } + }, false, true); + } + + /** + * Test record category as records manager + */ + public void testRecordCategoryAsRecordsManager() + { + retryingTransactionHelper.doInTransaction( + new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil + .setFullyAuthenticatedUser(recordsManagerName); + // permissionService.setPermission(recordCategory_1, + // rm_records_manager, FILING, true); + Map access = securityService + .getCapabilities(rmContainer); + assertEquals(65, access.size()); // 58 + File + check(access, ACCESS_AUDIT, AccessStatus.ALLOWED); + check(access, ADD_MODIFY_EVENT_DATES, + AccessStatus.DENIED); + check(access, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + AccessStatus.DENIED); + check(access, ATTACH_RULES_TO_METADATA_PROPERTIES, + AccessStatus.ALLOWED); + check(access, AUTHORIZE_ALL_TRANSFERS, + AccessStatus.DENIED); + check(access, AUTHORIZE_NOMINATED_TRANSFERS, + AccessStatus.DENIED); + check(access, CHANGE_OR_DELETE_REFERENCES, + AccessStatus.UNDETERMINED); + check(access, CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, CREATE_AND_ASSOCIATE_SELECTION_LISTS, + AccessStatus.ALLOWED); + check(access, + CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_EVENTS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_FOLDERS, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_RECORD_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_REFERENCE_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_ROLES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_TIMEFRAMES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, + AccessStatus.DENIED); + check(access, CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_AUDIT_AS_RECORD, + AccessStatus.ALLOWED); + check(access, DECLARE_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_RECORDS_IN_CLOSED_FOLDERS, + AccessStatus.DENIED); + check(access, DELETE_AUDIT, AccessStatus.ALLOWED); + check(access, DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, DELETE_RECORDS, AccessStatus.DENIED); + check(access, DESTROY_RECORDS, AccessStatus.DENIED); + check(access, + DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, + AccessStatus.DENIED); + check(access, DISPLAY_RIGHTS_REPORT, + AccessStatus.ALLOWED); + check(access, EDIT_DECLARED_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_NON_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, EDIT_SELECTION_LISTS, + AccessStatus.ALLOWED); + check(access, ENABLE_DISABLE_AUDIT_BY_TYPES, + AccessStatus.ALLOWED); + check(access, EXPORT_AUDIT, AccessStatus.ALLOWED); + check(access, EXTEND_RETENTION_PERIOD_OR_FREEZE, + AccessStatus.DENIED); + check(access, MAKE_OPTIONAL_PARAMETERS_MANDATORY, + AccessStatus.ALLOWED); + check(access, MANAGE_ACCESS_CONTROLS, + AccessStatus.DENIED); + check(access, MANAGE_ACCESS_RIGHTS, + AccessStatus.ALLOWED); + check(access, MANUALLY_CHANGE_DISPOSITION_DATES, + AccessStatus.DENIED); + check(access, MAP_CLASSIFICATION_GUIDE_METADATA, + AccessStatus.ALLOWED); + check(access, MAP_EMAIL_METADATA, AccessStatus.ALLOWED); + check(access, MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, PASSWORD_CONTROL, AccessStatus.ALLOWED); + check(access, PLANNING_REVIEW_CYCLES, + AccessStatus.DENIED); + check(access, RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, SELECT_AUDIT_METADATA, + AccessStatus.ALLOWED); + check(access, TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, UNFREEZE, AccessStatus.DENIED); + check(access, UPDATE_CLASSIFICATION_DATES, + AccessStatus.ALLOWED); + check(access, UPDATE_EXEMPTION_CATEGORIES, + AccessStatus.ALLOWED); + check(access, UPDATE_TRIGGER_DATES, + AccessStatus.ALLOWED); + check(access, UPDATE_VITAL_RECORD_CYCLE_INFORMATION, + AccessStatus.ALLOWED); + check(access, UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, + AccessStatus.ALLOWED); + check(access, VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, VIEW_UPDATE_REASONS_FOR_FREEZE, + AccessStatus.DENIED); + + return null; + } + }, false, true); + } + + /** + * Test record category as security officer + */ + public void testRecordCategoryAsSecurityOfficer() + { + retryingTransactionHelper.doInTransaction( + new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil + .setFullyAuthenticatedUser(securityOfficerName); + // permissionService.setPermission(recordCategory_1, + // securityOfficerName, FILING, true); + Map access = securityService + .getCapabilities(rmContainer); + assertEquals(65, access.size()); // 58 + File + check(access, ACCESS_AUDIT, AccessStatus.DENIED); + check(access, ADD_MODIFY_EVENT_DATES, + AccessStatus.DENIED); + check(access, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + AccessStatus.DENIED); + check(access, ATTACH_RULES_TO_METADATA_PROPERTIES, + AccessStatus.DENIED); + check(access, AUTHORIZE_ALL_TRANSFERS, + AccessStatus.DENIED); + check(access, AUTHORIZE_NOMINATED_TRANSFERS, + AccessStatus.DENIED); + check(access, CHANGE_OR_DELETE_REFERENCES, + AccessStatus.DENIED); + check(access, CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, CREATE_AND_ASSOCIATE_SELECTION_LISTS, + AccessStatus.DENIED); + check(access, + CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_EVENTS, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FOLDERS, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_RECORD_TYPES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_REFERENCE_TYPES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_ROLES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_TIMEFRAMES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, + AccessStatus.DENIED); + check(access, CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_AUDIT_AS_RECORD, + AccessStatus.DENIED); + check(access, DECLARE_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_RECORDS_IN_CLOSED_FOLDERS, + AccessStatus.DENIED); + check(access, DELETE_AUDIT, AccessStatus.DENIED); + check(access, DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, DELETE_RECORDS, AccessStatus.DENIED); + check(access, DESTROY_RECORDS, AccessStatus.DENIED); + check(access, + DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, + AccessStatus.DENIED); + check(access, DISPLAY_RIGHTS_REPORT, + AccessStatus.DENIED); + check(access, EDIT_DECLARED_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_NON_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, EDIT_SELECTION_LISTS, AccessStatus.DENIED); + check(access, ENABLE_DISABLE_AUDIT_BY_TYPES, + AccessStatus.DENIED); + check(access, EXPORT_AUDIT, AccessStatus.DENIED); + check(access, EXTEND_RETENTION_PERIOD_OR_FREEZE, + AccessStatus.DENIED); + check(access, MAKE_OPTIONAL_PARAMETERS_MANDATORY, + AccessStatus.DENIED); + check(access, MANAGE_ACCESS_CONTROLS, + AccessStatus.DENIED); + check(access, MANAGE_ACCESS_RIGHTS, AccessStatus.DENIED); + check(access, MANUALLY_CHANGE_DISPOSITION_DATES, + AccessStatus.DENIED); + check(access, MAP_CLASSIFICATION_GUIDE_METADATA, + AccessStatus.DENIED); + check(access, MAP_EMAIL_METADATA, AccessStatus.DENIED); + check(access, MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, PASSWORD_CONTROL, AccessStatus.DENIED); + check(access, PLANNING_REVIEW_CYCLES, + AccessStatus.DENIED); + check(access, RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, SELECT_AUDIT_METADATA, + AccessStatus.DENIED); + check(access, TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, UNFREEZE, AccessStatus.DENIED); + check(access, UPDATE_CLASSIFICATION_DATES, + AccessStatus.ALLOWED); + check(access, UPDATE_EXEMPTION_CATEGORIES, + AccessStatus.ALLOWED); + check(access, UPDATE_TRIGGER_DATES, AccessStatus.DENIED); + check(access, UPDATE_VITAL_RECORD_CYCLE_INFORMATION, + AccessStatus.DENIED); + check(access, UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, + AccessStatus.ALLOWED); + check(access, VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, VIEW_UPDATE_REASONS_FOR_FREEZE, + AccessStatus.DENIED); + + return null; + } + }, false, true); + } + + /** + * Test record category as power user + */ + public void testRecordCategoryAsPowerUser() + { + retryingTransactionHelper.doInTransaction( + new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil + .setFullyAuthenticatedUser(powerUserName); + // permissionService.setPermission(rmContainer, + // powerUserName, FILING, true); + Map access = securityService + .getCapabilities(rmContainer); + assertEquals(65, access.size()); // 58 + File + check(access, ACCESS_AUDIT, AccessStatus.DENIED); + check(access, ADD_MODIFY_EVENT_DATES, + AccessStatus.DENIED); + check(access, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + AccessStatus.DENIED); + check(access, ATTACH_RULES_TO_METADATA_PROPERTIES, + AccessStatus.DENIED); + check(access, AUTHORIZE_ALL_TRANSFERS, + AccessStatus.DENIED); + check(access, AUTHORIZE_NOMINATED_TRANSFERS, + AccessStatus.DENIED); + check(access, CHANGE_OR_DELETE_REFERENCES, + AccessStatus.DENIED); + check(access, CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, CREATE_AND_ASSOCIATE_SELECTION_LISTS, + AccessStatus.DENIED); + check(access, + CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_EVENTS, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FOLDERS, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_RECORD_TYPES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_REFERENCE_TYPES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_ROLES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_TIMEFRAMES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, + AccessStatus.DENIED); + check(access, CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_AUDIT_AS_RECORD, + AccessStatus.DENIED); + check(access, DECLARE_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_RECORDS_IN_CLOSED_FOLDERS, + AccessStatus.DENIED); + check(access, DELETE_AUDIT, AccessStatus.DENIED); + check(access, DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, DELETE_RECORDS, AccessStatus.DENIED); + check(access, DESTROY_RECORDS, AccessStatus.DENIED); + check(access, + DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, + AccessStatus.DENIED); + check(access, DISPLAY_RIGHTS_REPORT, + AccessStatus.DENIED); + check(access, EDIT_DECLARED_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_NON_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, EDIT_SELECTION_LISTS, AccessStatus.DENIED); + check(access, ENABLE_DISABLE_AUDIT_BY_TYPES, + AccessStatus.DENIED); + check(access, EXPORT_AUDIT, AccessStatus.DENIED); + check(access, EXTEND_RETENTION_PERIOD_OR_FREEZE, + AccessStatus.DENIED); + check(access, MAKE_OPTIONAL_PARAMETERS_MANDATORY, + AccessStatus.DENIED); + check(access, MANAGE_ACCESS_CONTROLS, + AccessStatus.DENIED); + check(access, MANAGE_ACCESS_RIGHTS, AccessStatus.DENIED); + check(access, MANUALLY_CHANGE_DISPOSITION_DATES, + AccessStatus.DENIED); + check(access, MAP_CLASSIFICATION_GUIDE_METADATA, + AccessStatus.DENIED); + check(access, MAP_EMAIL_METADATA, AccessStatus.DENIED); + check(access, MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, PASSWORD_CONTROL, AccessStatus.DENIED); + check(access, PLANNING_REVIEW_CYCLES, + AccessStatus.DENIED); + check(access, RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, SELECT_AUDIT_METADATA, + AccessStatus.DENIED); + check(access, TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, UNFREEZE, AccessStatus.DENIED); + check(access, UPDATE_CLASSIFICATION_DATES, + AccessStatus.DENIED); + check(access, UPDATE_EXEMPTION_CATEGORIES, + AccessStatus.DENIED); + check(access, UPDATE_TRIGGER_DATES, AccessStatus.DENIED); + check(access, UPDATE_VITAL_RECORD_CYCLE_INFORMATION, + AccessStatus.DENIED); + check(access, UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, + AccessStatus.DENIED); + check(access, VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, VIEW_UPDATE_REASONS_FOR_FREEZE, + AccessStatus.DENIED); + + return null; + } + }, false, true); + } + + /** + * Test record category as user + */ + public void testRecordCategoryAsUser() + { + retryingTransactionHelper.doInTransaction( + new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil + .setFullyAuthenticatedUser(rmUserName); + // permissionService.setPermission(rmContainer, + // rmUserName, FILING, true); + Map access = securityService + .getCapabilities(rmContainer); + assertEquals(65, access.size()); // 58 + File + check(access, ACCESS_AUDIT, AccessStatus.DENIED); + check(access, ADD_MODIFY_EVENT_DATES, + AccessStatus.DENIED); + check(access, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + AccessStatus.DENIED); + check(access, ATTACH_RULES_TO_METADATA_PROPERTIES, + AccessStatus.DENIED); + check(access, AUTHORIZE_ALL_TRANSFERS, + AccessStatus.DENIED); + check(access, AUTHORIZE_NOMINATED_TRANSFERS, + AccessStatus.DENIED); + check(access, CHANGE_OR_DELETE_REFERENCES, + AccessStatus.DENIED); + check(access, CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, CREATE_AND_ASSOCIATE_SELECTION_LISTS, + AccessStatus.DENIED); + check(access, + CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_EVENTS, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FOLDERS, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_RECORD_TYPES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_REFERENCE_TYPES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_ROLES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_TIMEFRAMES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, + AccessStatus.DENIED); + check(access, CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_AUDIT_AS_RECORD, + AccessStatus.DENIED); + check(access, DECLARE_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_RECORDS_IN_CLOSED_FOLDERS, + AccessStatus.DENIED); + check(access, DELETE_AUDIT, AccessStatus.DENIED); + check(access, DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, DELETE_RECORDS, AccessStatus.DENIED); + check(access, DESTROY_RECORDS, AccessStatus.DENIED); + check(access, + DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, + AccessStatus.DENIED); + check(access, DISPLAY_RIGHTS_REPORT, + AccessStatus.DENIED); + check(access, EDIT_DECLARED_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_NON_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, EDIT_SELECTION_LISTS, AccessStatus.DENIED); + check(access, ENABLE_DISABLE_AUDIT_BY_TYPES, + AccessStatus.DENIED); + check(access, EXPORT_AUDIT, AccessStatus.DENIED); + check(access, EXTEND_RETENTION_PERIOD_OR_FREEZE, + AccessStatus.DENIED); + check(access, MAKE_OPTIONAL_PARAMETERS_MANDATORY, + AccessStatus.DENIED); + check(access, MANAGE_ACCESS_CONTROLS, + AccessStatus.DENIED); + check(access, MANAGE_ACCESS_RIGHTS, AccessStatus.DENIED); + check(access, MANUALLY_CHANGE_DISPOSITION_DATES, + AccessStatus.DENIED); + check(access, MAP_CLASSIFICATION_GUIDE_METADATA, + AccessStatus.DENIED); + check(access, MAP_EMAIL_METADATA, AccessStatus.DENIED); + check(access, MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, PASSWORD_CONTROL, AccessStatus.DENIED); + check(access, PLANNING_REVIEW_CYCLES, + AccessStatus.DENIED); + check(access, RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, SELECT_AUDIT_METADATA, + AccessStatus.DENIED); + check(access, TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, UNFREEZE, AccessStatus.DENIED); + check(access, UPDATE_CLASSIFICATION_DATES, + AccessStatus.DENIED); + check(access, UPDATE_EXEMPTION_CATEGORIES, + AccessStatus.DENIED); + check(access, UPDATE_TRIGGER_DATES, AccessStatus.DENIED); + check(access, UPDATE_VITAL_RECORD_CYCLE_INFORMATION, + AccessStatus.DENIED); + check(access, UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, + AccessStatus.DENIED); + check(access, VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, VIEW_UPDATE_REASONS_FOR_FREEZE, + AccessStatus.DENIED); + + return null; + } + }, false, true); + } + + /** + * Test record folder as system + */ + public void testRecordFolderAsSystem() + { + retryingTransactionHelper.doInTransaction( + new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil + .setFullyAuthenticatedUser(AuthenticationUtil.SYSTEM_USER_NAME); + + Map access = securityService + .getCapabilities(rmFolder); + assertEquals(65, access.size()); + check(access, ACCESS_AUDIT, AccessStatus.ALLOWED); + check(access, ADD_MODIFY_EVENT_DATES, + AccessStatus.ALLOWED); + check(access, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + AccessStatus.DENIED); + check(access, ATTACH_RULES_TO_METADATA_PROPERTIES, + AccessStatus.ALLOWED); + check(access, AUTHORIZE_ALL_TRANSFERS, + AccessStatus.DENIED); + check(access, AUTHORIZE_NOMINATED_TRANSFERS, + AccessStatus.DENIED); + check(access, CHANGE_OR_DELETE_REFERENCES, + AccessStatus.UNDETERMINED); + check(access, CLOSE_FOLDERS, AccessStatus.ALLOWED); + check(access, CREATE_AND_ASSOCIATE_SELECTION_LISTS, + AccessStatus.ALLOWED); + check(access, + CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_EVENTS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_FOLDERS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_RECORD_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_REFERENCE_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_ROLES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_TIMEFRAMES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, + AccessStatus.DENIED); + check(access, CYCLE_VITAL_RECORDS, AccessStatus.DENIED); // rmFolder + // is + // not + // a + // vital + // record + check(access, DECLARE_AUDIT_AS_RECORD, + AccessStatus.ALLOWED); + check(access, DECLARE_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_RECORDS_IN_CLOSED_FOLDERS, + AccessStatus.DENIED); + check(access, DELETE_AUDIT, AccessStatus.ALLOWED); + check(access, DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, DELETE_RECORDS, AccessStatus.DENIED); + check(access, DESTROY_RECORDS, AccessStatus.ALLOWED); + check(access, + DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, + AccessStatus.DENIED); + check(access, DISPLAY_RIGHTS_REPORT, + AccessStatus.ALLOWED); + check(access, EDIT_DECLARED_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_NON_RECORD_METADATA, + AccessStatus.ALLOWED); + check(access, EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, EDIT_SELECTION_LISTS, + AccessStatus.ALLOWED); + check(access, ENABLE_DISABLE_AUDIT_BY_TYPES, + AccessStatus.ALLOWED); + check(access, EXPORT_AUDIT, AccessStatus.ALLOWED); + check(access, EXTEND_RETENTION_PERIOD_OR_FREEZE, + AccessStatus.ALLOWED); + check(access, MAKE_OPTIONAL_PARAMETERS_MANDATORY, + AccessStatus.ALLOWED); + check(access, MANAGE_ACCESS_CONTROLS, + AccessStatus.ALLOWED); + check(access, MANAGE_ACCESS_RIGHTS, + AccessStatus.ALLOWED); + check(access, MANUALLY_CHANGE_DISPOSITION_DATES, + AccessStatus.ALLOWED); + check(access, MAP_CLASSIFICATION_GUIDE_METADATA, + AccessStatus.ALLOWED); + check(access, MAP_EMAIL_METADATA, AccessStatus.ALLOWED); + check(access, MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, PASSWORD_CONTROL, AccessStatus.ALLOWED); + check(access, PLANNING_REVIEW_CYCLES, + AccessStatus.DENIED); + check(access, RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, SELECT_AUDIT_METADATA, + AccessStatus.ALLOWED); + check(access, TRIGGER_AN_EVENT, AccessStatus.ALLOWED); + check(access, UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, UNFREEZE, AccessStatus.DENIED); + check(access, UPDATE_CLASSIFICATION_DATES, + AccessStatus.ALLOWED); + check(access, UPDATE_EXEMPTION_CATEGORIES, + AccessStatus.ALLOWED); + check(access, UPDATE_TRIGGER_DATES, + AccessStatus.ALLOWED); + check(access, UPDATE_VITAL_RECORD_CYCLE_INFORMATION, + AccessStatus.ALLOWED); + check(access, UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, + AccessStatus.ALLOWED); + check(access, VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, VIEW_UPDATE_REASONS_FOR_FREEZE, + AccessStatus.DENIED); + + return null; + } + }, false, true); + + } + + /** + * Test record folder as admin + */ + public void testRecordFolderAsAdmin() + { + retryingTransactionHelper.doInTransaction( + new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil + .setFullyAuthenticatedUser(AuthenticationUtil + .getAdminUserName()); + Map access = securityService + .getCapabilities(rmFolder); + assertEquals(65, access.size()); + check(access, ACCESS_AUDIT, AccessStatus.ALLOWED); + check(access, ADD_MODIFY_EVENT_DATES, + AccessStatus.ALLOWED); + check(access, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + AccessStatus.DENIED); + check(access, ATTACH_RULES_TO_METADATA_PROPERTIES, + AccessStatus.ALLOWED); + check(access, AUTHORIZE_ALL_TRANSFERS, + AccessStatus.DENIED); + check(access, AUTHORIZE_NOMINATED_TRANSFERS, + AccessStatus.DENIED); + check(access, CHANGE_OR_DELETE_REFERENCES, + AccessStatus.UNDETERMINED); + check(access, CLOSE_FOLDERS, AccessStatus.ALLOWED); + check(access, CREATE_AND_ASSOCIATE_SELECTION_LISTS, + AccessStatus.ALLOWED); + check(access, + CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_EVENTS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_FOLDERS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_RECORD_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_REFERENCE_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_ROLES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_TIMEFRAMES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, + AccessStatus.DENIED); + check(access, CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_AUDIT_AS_RECORD, + AccessStatus.ALLOWED); + check(access, DECLARE_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_RECORDS_IN_CLOSED_FOLDERS, + AccessStatus.DENIED); + check(access, DELETE_AUDIT, AccessStatus.ALLOWED); + check(access, DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, DELETE_RECORDS, AccessStatus.DENIED); + check(access, DESTROY_RECORDS, AccessStatus.ALLOWED); + check(access, + DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, + AccessStatus.DENIED); + check(access, DISPLAY_RIGHTS_REPORT, + AccessStatus.ALLOWED); + check(access, EDIT_DECLARED_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_NON_RECORD_METADATA, + AccessStatus.ALLOWED); + check(access, EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, EDIT_SELECTION_LISTS, + AccessStatus.ALLOWED); + check(access, ENABLE_DISABLE_AUDIT_BY_TYPES, + AccessStatus.ALLOWED); + check(access, EXPORT_AUDIT, AccessStatus.ALLOWED); + check(access, EXTEND_RETENTION_PERIOD_OR_FREEZE, + AccessStatus.ALLOWED); + check(access, MAKE_OPTIONAL_PARAMETERS_MANDATORY, + AccessStatus.ALLOWED); + check(access, MANAGE_ACCESS_CONTROLS, + AccessStatus.ALLOWED); + check(access, MANAGE_ACCESS_RIGHTS, + AccessStatus.ALLOWED); + check(access, MANUALLY_CHANGE_DISPOSITION_DATES, + AccessStatus.ALLOWED); + check(access, MAP_CLASSIFICATION_GUIDE_METADATA, + AccessStatus.ALLOWED); + check(access, MAP_EMAIL_METADATA, AccessStatus.ALLOWED); + check(access, MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, PASSWORD_CONTROL, AccessStatus.ALLOWED); + check(access, PLANNING_REVIEW_CYCLES, + AccessStatus.DENIED); + check(access, RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, SELECT_AUDIT_METADATA, + AccessStatus.ALLOWED); + check(access, TRIGGER_AN_EVENT, AccessStatus.ALLOWED); + check(access, UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, UNFREEZE, AccessStatus.DENIED); + check(access, UPDATE_CLASSIFICATION_DATES, + AccessStatus.ALLOWED); + check(access, UPDATE_EXEMPTION_CATEGORIES, + AccessStatus.ALLOWED); + check(access, UPDATE_TRIGGER_DATES, + AccessStatus.ALLOWED); + check(access, UPDATE_VITAL_RECORD_CYCLE_INFORMATION, + AccessStatus.ALLOWED); + check(access, UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, + AccessStatus.ALLOWED); + check(access, VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, VIEW_UPDATE_REASONS_FOR_FREEZE, + AccessStatus.DENIED); + + return null; + } + }, false, true); + + } + + /** + * Test record folder as administrator + */ + public void testRecordFolderAsAdministrator() + { + retryingTransactionHelper.doInTransaction( + new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil + .setFullyAuthenticatedUser(rmAdminName); + Map access = securityService + .getCapabilities(rmFolder); + assertEquals(65, access.size()); + check(access, ACCESS_AUDIT, AccessStatus.ALLOWED); + check(access, ADD_MODIFY_EVENT_DATES, + AccessStatus.ALLOWED); + check(access, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + AccessStatus.DENIED); + check(access, ATTACH_RULES_TO_METADATA_PROPERTIES, + AccessStatus.ALLOWED); + check(access, AUTHORIZE_ALL_TRANSFERS, + AccessStatus.DENIED); + check(access, AUTHORIZE_NOMINATED_TRANSFERS, + AccessStatus.DENIED); + check(access, CHANGE_OR_DELETE_REFERENCES, + AccessStatus.UNDETERMINED); + check(access, CLOSE_FOLDERS, AccessStatus.ALLOWED); + check(access, CREATE_AND_ASSOCIATE_SELECTION_LISTS, + AccessStatus.ALLOWED); + check(access, + CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_EVENTS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_FOLDERS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_RECORD_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_REFERENCE_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_ROLES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_TIMEFRAMES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, + AccessStatus.DENIED); + check(access, CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_AUDIT_AS_RECORD, + AccessStatus.ALLOWED); + check(access, DECLARE_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_RECORDS_IN_CLOSED_FOLDERS, + AccessStatus.DENIED); + check(access, DELETE_AUDIT, AccessStatus.ALLOWED); + check(access, DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, DELETE_RECORDS, AccessStatus.DENIED); + check(access, DESTROY_RECORDS, AccessStatus.ALLOWED); + check(access, + DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, + AccessStatus.DENIED); + check(access, DISPLAY_RIGHTS_REPORT, + AccessStatus.ALLOWED); + check(access, EDIT_DECLARED_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_NON_RECORD_METADATA, + AccessStatus.ALLOWED); + check(access, EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, EDIT_SELECTION_LISTS, + AccessStatus.ALLOWED); + check(access, ENABLE_DISABLE_AUDIT_BY_TYPES, + AccessStatus.ALLOWED); + check(access, EXPORT_AUDIT, AccessStatus.ALLOWED); + check(access, EXTEND_RETENTION_PERIOD_OR_FREEZE, + AccessStatus.ALLOWED); + check(access, MAKE_OPTIONAL_PARAMETERS_MANDATORY, + AccessStatus.ALLOWED); + check(access, MANAGE_ACCESS_CONTROLS, + AccessStatus.ALLOWED); + check(access, MANAGE_ACCESS_RIGHTS, + AccessStatus.ALLOWED); + check(access, MANUALLY_CHANGE_DISPOSITION_DATES, + AccessStatus.ALLOWED); + check(access, MAP_CLASSIFICATION_GUIDE_METADATA, + AccessStatus.ALLOWED); + check(access, MAP_EMAIL_METADATA, AccessStatus.ALLOWED); + check(access, MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, PASSWORD_CONTROL, AccessStatus.ALLOWED); + check(access, PLANNING_REVIEW_CYCLES, + AccessStatus.DENIED); + check(access, RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, SELECT_AUDIT_METADATA, + AccessStatus.ALLOWED); + check(access, TRIGGER_AN_EVENT, AccessStatus.ALLOWED); + check(access, UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, UNFREEZE, AccessStatus.DENIED); + check(access, UPDATE_CLASSIFICATION_DATES, + AccessStatus.ALLOWED); + check(access, UPDATE_EXEMPTION_CATEGORIES, + AccessStatus.ALLOWED); + check(access, UPDATE_TRIGGER_DATES, + AccessStatus.ALLOWED); + check(access, UPDATE_VITAL_RECORD_CYCLE_INFORMATION, + AccessStatus.ALLOWED); + check(access, UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, + AccessStatus.ALLOWED); + check(access, VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, VIEW_UPDATE_REASONS_FOR_FREEZE, + AccessStatus.DENIED); + + return null; + } + }, false, true); + } + + /** + * Test record folder as records manager + */ + public void testRecordFolderAsRecordsManager() + { + retryingTransactionHelper.doInTransaction( + new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil.setFullyAuthenticatedUser(recordsManagerName); + //setFilingOnRecordFolder(rmFolder, recordsManagerName); + Map access = securityService.getCapabilities(rmFolder); + assertEquals(65, access.size()); // 58 + File + check(access, ACCESS_AUDIT, AccessStatus.ALLOWED); + check(access, ADD_MODIFY_EVENT_DATES, + AccessStatus.ALLOWED); + check(access, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + AccessStatus.DENIED); + check(access, ATTACH_RULES_TO_METADATA_PROPERTIES, + AccessStatus.ALLOWED); + check(access, AUTHORIZE_ALL_TRANSFERS, + AccessStatus.DENIED); + check(access, AUTHORIZE_NOMINATED_TRANSFERS, + AccessStatus.DENIED); + check(access, CHANGE_OR_DELETE_REFERENCES, + AccessStatus.UNDETERMINED); + check(access, CLOSE_FOLDERS, AccessStatus.ALLOWED); + check(access, CREATE_AND_ASSOCIATE_SELECTION_LISTS, + AccessStatus.ALLOWED); + check(access, + CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_EVENTS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_FOLDERS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_RECORD_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_REFERENCE_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_ROLES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_TIMEFRAMES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, + AccessStatus.DENIED); + check(access, CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_AUDIT_AS_RECORD, + AccessStatus.ALLOWED); + check(access, DECLARE_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_RECORDS_IN_CLOSED_FOLDERS, + AccessStatus.DENIED); + check(access, DELETE_AUDIT, AccessStatus.ALLOWED); + check(access, DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, DELETE_RECORDS, AccessStatus.DENIED); + check(access, DESTROY_RECORDS, AccessStatus.ALLOWED); + check(access, + DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, + AccessStatus.DENIED); + check(access, DISPLAY_RIGHTS_REPORT, + AccessStatus.ALLOWED); + check(access, EDIT_DECLARED_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_NON_RECORD_METADATA, + AccessStatus.ALLOWED); + check(access, EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, EDIT_SELECTION_LISTS, + AccessStatus.ALLOWED); + check(access, ENABLE_DISABLE_AUDIT_BY_TYPES, + AccessStatus.ALLOWED); + check(access, EXPORT_AUDIT, AccessStatus.ALLOWED); + check(access, EXTEND_RETENTION_PERIOD_OR_FREEZE, + AccessStatus.ALLOWED); + check(access, MAKE_OPTIONAL_PARAMETERS_MANDATORY, + AccessStatus.ALLOWED); + check(access, MANAGE_ACCESS_CONTROLS, + AccessStatus.DENIED); + check(access, MANAGE_ACCESS_RIGHTS, + AccessStatus.ALLOWED); + check(access, MANUALLY_CHANGE_DISPOSITION_DATES, + AccessStatus.ALLOWED); + check(access, MAP_CLASSIFICATION_GUIDE_METADATA, + AccessStatus.ALLOWED); + check(access, MAP_EMAIL_METADATA, AccessStatus.ALLOWED); + check(access, MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, PASSWORD_CONTROL, AccessStatus.ALLOWED); + check(access, PLANNING_REVIEW_CYCLES, + AccessStatus.DENIED); + check(access, RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, SELECT_AUDIT_METADATA, + AccessStatus.ALLOWED); + check(access, TRIGGER_AN_EVENT, AccessStatus.ALLOWED); + check(access, UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, UNFREEZE, AccessStatus.DENIED); + check(access, UPDATE_CLASSIFICATION_DATES, + AccessStatus.ALLOWED); + check(access, UPDATE_EXEMPTION_CATEGORIES, + AccessStatus.ALLOWED); + check(access, UPDATE_TRIGGER_DATES, + AccessStatus.ALLOWED); + check(access, UPDATE_VITAL_RECORD_CYCLE_INFORMATION, + AccessStatus.ALLOWED); + check(access, UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, + AccessStatus.ALLOWED); + check(access, VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, VIEW_UPDATE_REASONS_FOR_FREEZE, + AccessStatus.DENIED); + + return null; + } + }, false, true); + } + + /** + * Test record folder as security officer + */ + public void testRecordFolderAsSecurityOfficer() + { + retryingTransactionHelper.doInTransaction( + new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil.setFullyAuthenticatedUser(securityOfficerName); + //setFilingOnRecordFolder(rmFolder, securityOfficerName); + Map access = securityService.getCapabilities(rmFolder); + assertEquals(65, access.size()); // 58 + File + check(access, ACCESS_AUDIT, AccessStatus.DENIED); + check(access, ADD_MODIFY_EVENT_DATES, + AccessStatus.ALLOWED); + check(access, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + AccessStatus.DENIED); + check(access, ATTACH_RULES_TO_METADATA_PROPERTIES, + AccessStatus.DENIED); + check(access, AUTHORIZE_ALL_TRANSFERS, + AccessStatus.DENIED); + check(access, AUTHORIZE_NOMINATED_TRANSFERS, + AccessStatus.DENIED); + check(access, CHANGE_OR_DELETE_REFERENCES, + AccessStatus.DENIED); + check(access, CLOSE_FOLDERS, AccessStatus.ALLOWED); + check(access, CREATE_AND_ASSOCIATE_SELECTION_LISTS, + AccessStatus.DENIED); + check(access, + CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_EVENTS, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FOLDERS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_RECORD_TYPES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_REFERENCE_TYPES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_ROLES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_TIMEFRAMES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, + AccessStatus.DENIED); + check(access, CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_AUDIT_AS_RECORD, + AccessStatus.DENIED); + check(access, DECLARE_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_RECORDS_IN_CLOSED_FOLDERS, + AccessStatus.DENIED); + check(access, DELETE_AUDIT, AccessStatus.DENIED); + check(access, DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, DELETE_RECORDS, AccessStatus.DENIED); + check(access, DESTROY_RECORDS, AccessStatus.DENIED); + check(access, + DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, + AccessStatus.DENIED); + check(access, DISPLAY_RIGHTS_REPORT, + AccessStatus.DENIED); + check(access, EDIT_DECLARED_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_NON_RECORD_METADATA, + AccessStatus.ALLOWED); + check(access, EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, EDIT_SELECTION_LISTS, AccessStatus.DENIED); + check(access, ENABLE_DISABLE_AUDIT_BY_TYPES, + AccessStatus.DENIED); + check(access, EXPORT_AUDIT, AccessStatus.DENIED); + check(access, EXTEND_RETENTION_PERIOD_OR_FREEZE, + AccessStatus.DENIED); + check(access, MAKE_OPTIONAL_PARAMETERS_MANDATORY, + AccessStatus.DENIED); + check(access, MANAGE_ACCESS_CONTROLS, + AccessStatus.DENIED); + check(access, MANAGE_ACCESS_RIGHTS, AccessStatus.DENIED); + check(access, MANUALLY_CHANGE_DISPOSITION_DATES, + AccessStatus.DENIED); + check(access, MAP_CLASSIFICATION_GUIDE_METADATA, + AccessStatus.DENIED); + check(access, MAP_EMAIL_METADATA, AccessStatus.DENIED); + check(access, MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, PASSWORD_CONTROL, AccessStatus.DENIED); + check(access, PLANNING_REVIEW_CYCLES, + AccessStatus.DENIED); + check(access, RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, SELECT_AUDIT_METADATA, + AccessStatus.DENIED); + check(access, TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, UNFREEZE, AccessStatus.DENIED); + check(access, UPDATE_CLASSIFICATION_DATES, + AccessStatus.ALLOWED); + check(access, UPDATE_EXEMPTION_CATEGORIES, + AccessStatus.ALLOWED); + check(access, UPDATE_TRIGGER_DATES, AccessStatus.DENIED); + check(access, UPDATE_VITAL_RECORD_CYCLE_INFORMATION, + AccessStatus.DENIED); + check(access, UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, + AccessStatus.ALLOWED); + check(access, VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, VIEW_UPDATE_REASONS_FOR_FREEZE, + AccessStatus.DENIED); + + return null; + } + }, false, true); + } + + /** + * Test record folder as power user + */ + public void testRecordFolderAsPowerUser() + { + retryingTransactionHelper.doInTransaction( + new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil.setFullyAuthenticatedUser(powerUserName); + //setFilingOnRecordFolder(rmFolder, powerUserName); + Map access = securityService.getCapabilities(rmFolder); + assertEquals(65, access.size()); // 58 + File + check(access, ACCESS_AUDIT, AccessStatus.DENIED); + check(access, ADD_MODIFY_EVENT_DATES, + AccessStatus.ALLOWED); + check(access, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + AccessStatus.DENIED); + check(access, ATTACH_RULES_TO_METADATA_PROPERTIES, + AccessStatus.DENIED); + check(access, AUTHORIZE_ALL_TRANSFERS, + AccessStatus.DENIED); + check(access, AUTHORIZE_NOMINATED_TRANSFERS, + AccessStatus.DENIED); + check(access, CHANGE_OR_DELETE_REFERENCES, + AccessStatus.DENIED); + check(access, CLOSE_FOLDERS, AccessStatus.ALLOWED); + check(access, CREATE_AND_ASSOCIATE_SELECTION_LISTS, + AccessStatus.DENIED); + check(access, + CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_EVENTS, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FOLDERS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_RECORD_TYPES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_REFERENCE_TYPES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_ROLES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_TIMEFRAMES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, + AccessStatus.DENIED); + check(access, CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_AUDIT_AS_RECORD, + AccessStatus.DENIED); + check(access, DECLARE_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_RECORDS_IN_CLOSED_FOLDERS, + AccessStatus.DENIED); + check(access, DELETE_AUDIT, AccessStatus.DENIED); + check(access, DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, DELETE_RECORDS, AccessStatus.DENIED); + check(access, DESTROY_RECORDS, AccessStatus.DENIED); + check(access, + DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, + AccessStatus.DENIED); + check(access, DISPLAY_RIGHTS_REPORT, + AccessStatus.DENIED); + check(access, EDIT_DECLARED_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_NON_RECORD_METADATA, + AccessStatus.ALLOWED); + check(access, EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, EDIT_SELECTION_LISTS, AccessStatus.DENIED); + check(access, ENABLE_DISABLE_AUDIT_BY_TYPES, + AccessStatus.DENIED); + check(access, EXPORT_AUDIT, AccessStatus.DENIED); + check(access, EXTEND_RETENTION_PERIOD_OR_FREEZE, + AccessStatus.DENIED); + check(access, MAKE_OPTIONAL_PARAMETERS_MANDATORY, + AccessStatus.DENIED); + check(access, MANAGE_ACCESS_CONTROLS, + AccessStatus.DENIED); + check(access, MANAGE_ACCESS_RIGHTS, AccessStatus.DENIED); + check(access, MANUALLY_CHANGE_DISPOSITION_DATES, + AccessStatus.DENIED); + check(access, MAP_CLASSIFICATION_GUIDE_METADATA, + AccessStatus.DENIED); + check(access, MAP_EMAIL_METADATA, AccessStatus.DENIED); + check(access, MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, PASSWORD_CONTROL, AccessStatus.DENIED); + check(access, PLANNING_REVIEW_CYCLES, + AccessStatus.DENIED); + check(access, RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, SELECT_AUDIT_METADATA, + AccessStatus.DENIED); + check(access, TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, UNFREEZE, AccessStatus.DENIED); + check(access, UPDATE_CLASSIFICATION_DATES, + AccessStatus.DENIED); + check(access, UPDATE_EXEMPTION_CATEGORIES, + AccessStatus.DENIED); + check(access, UPDATE_TRIGGER_DATES, AccessStatus.DENIED); + check(access, UPDATE_VITAL_RECORD_CYCLE_INFORMATION, + AccessStatus.DENIED); + check(access, UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, + AccessStatus.DENIED); + check(access, VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, VIEW_UPDATE_REASONS_FOR_FREEZE, + AccessStatus.DENIED); + + return null; + } + }, false, true); + } + + /** + * Test record folder as user + */ + public void testRecordFolderAsUser() + { + retryingTransactionHelper.doInTransaction( + new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil + .setFullyAuthenticatedUser(rmUserName); + //setFilingOnRecordFolder(rmFolder, rmUserName); + Map access = securityService + .getCapabilities(rmFolder); + assertEquals(65, access.size()); // 58 + File + check(access, ACCESS_AUDIT, AccessStatus.DENIED); + check(access, ADD_MODIFY_EVENT_DATES, + AccessStatus.DENIED); + check(access, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + AccessStatus.DENIED); + check(access, ATTACH_RULES_TO_METADATA_PROPERTIES, + AccessStatus.DENIED); + check(access, AUTHORIZE_ALL_TRANSFERS, + AccessStatus.DENIED); + check(access, AUTHORIZE_NOMINATED_TRANSFERS, + AccessStatus.DENIED); + check(access, CHANGE_OR_DELETE_REFERENCES, + AccessStatus.DENIED); + check(access, CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, CREATE_AND_ASSOCIATE_SELECTION_LISTS, + AccessStatus.DENIED); + check(access, + CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_EVENTS, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FOLDERS, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_RECORD_TYPES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_REFERENCE_TYPES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_ROLES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_TIMEFRAMES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, + AccessStatus.DENIED); + check(access, CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_AUDIT_AS_RECORD, + AccessStatus.DENIED); + check(access, DECLARE_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_RECORDS_IN_CLOSED_FOLDERS, + AccessStatus.DENIED); + check(access, DELETE_AUDIT, AccessStatus.DENIED); + check(access, DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, DELETE_RECORDS, AccessStatus.DENIED); + check(access, DESTROY_RECORDS, AccessStatus.DENIED); + check(access, + DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, + AccessStatus.DENIED); + check(access, DISPLAY_RIGHTS_REPORT, + AccessStatus.DENIED); + check(access, EDIT_DECLARED_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_NON_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, EDIT_SELECTION_LISTS, AccessStatus.DENIED); + check(access, ENABLE_DISABLE_AUDIT_BY_TYPES, + AccessStatus.DENIED); + check(access, EXPORT_AUDIT, AccessStatus.DENIED); + check(access, EXTEND_RETENTION_PERIOD_OR_FREEZE, + AccessStatus.DENIED); + check(access, MAKE_OPTIONAL_PARAMETERS_MANDATORY, + AccessStatus.DENIED); + check(access, MANAGE_ACCESS_CONTROLS, + AccessStatus.DENIED); + check(access, MANAGE_ACCESS_RIGHTS, AccessStatus.DENIED); + check(access, MANUALLY_CHANGE_DISPOSITION_DATES, + AccessStatus.DENIED); + check(access, MAP_CLASSIFICATION_GUIDE_METADATA, + AccessStatus.DENIED); + check(access, MAP_EMAIL_METADATA, AccessStatus.DENIED); + check(access, MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, PASSWORD_CONTROL, AccessStatus.DENIED); + check(access, PLANNING_REVIEW_CYCLES, + AccessStatus.DENIED); + check(access, RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, SELECT_AUDIT_METADATA, + AccessStatus.DENIED); + check(access, TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, UNFREEZE, AccessStatus.DENIED); + check(access, UPDATE_CLASSIFICATION_DATES, + AccessStatus.DENIED); + check(access, UPDATE_EXEMPTION_CATEGORIES, + AccessStatus.DENIED); + check(access, UPDATE_TRIGGER_DATES, AccessStatus.DENIED); + check(access, UPDATE_VITAL_RECORD_CYCLE_INFORMATION, + AccessStatus.DENIED); + check(access, UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, + AccessStatus.DENIED); + check(access, VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, VIEW_UPDATE_REASONS_FOR_FREEZE, + AccessStatus.DENIED); + + return null; + } + }, false, true); + } + + /** + * Test record as system + */ + public void testRecordAsSystem() + { + retryingTransactionHelper.doInTransaction( + new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.SYSTEM_USER_NAME); + Map access = securityService.getCapabilities(record); + assertEquals(65, access.size()); + check(access, ACCESS_AUDIT, AccessStatus.ALLOWED); + check(access, ADD_MODIFY_EVENT_DATES, + AccessStatus.DENIED); + check(access, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + AccessStatus.DENIED); + check(access, ATTACH_RULES_TO_METADATA_PROPERTIES, + AccessStatus.ALLOWED); + check(access, AUTHORIZE_ALL_TRANSFERS, + AccessStatus.DENIED); + check(access, AUTHORIZE_NOMINATED_TRANSFERS, + AccessStatus.DENIED); + check(access, CHANGE_OR_DELETE_REFERENCES, + AccessStatus.UNDETERMINED); + check(access, CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, CREATE_AND_ASSOCIATE_SELECTION_LISTS, + AccessStatus.ALLOWED); + check(access, + CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_EVENTS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_FOLDERS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_RECORD_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_REFERENCE_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_ROLES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_TIMEFRAMES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, + AccessStatus.DENIED); + check(access, CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_AUDIT_AS_RECORD, + AccessStatus.ALLOWED); + check(access, DECLARE_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_RECORDS_IN_CLOSED_FOLDERS, + AccessStatus.DENIED); + check(access, DELETE_AUDIT, AccessStatus.ALLOWED); + check(access, DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, DELETE_RECORDS, AccessStatus.ALLOWED); + check(access, DESTROY_RECORDS, AccessStatus.DENIED); + check(access, + DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, + AccessStatus.DENIED); + check(access, DISPLAY_RIGHTS_REPORT, + AccessStatus.ALLOWED); + check(access, EDIT_DECLARED_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_NON_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_RECORD_METADATA, + AccessStatus.ALLOWED); + check(access, EDIT_SELECTION_LISTS, + AccessStatus.ALLOWED); + check(access, ENABLE_DISABLE_AUDIT_BY_TYPES, + AccessStatus.ALLOWED); + check(access, EXPORT_AUDIT, AccessStatus.ALLOWED); + check(access, EXTEND_RETENTION_PERIOD_OR_FREEZE, + AccessStatus.ALLOWED); + check(access, MAKE_OPTIONAL_PARAMETERS_MANDATORY, + AccessStatus.ALLOWED); + check(access, MANAGE_ACCESS_CONTROLS, + AccessStatus.ALLOWED); + check(access, MANAGE_ACCESS_RIGHTS, + AccessStatus.ALLOWED); + check(access, MANUALLY_CHANGE_DISPOSITION_DATES, + AccessStatus.DENIED); + check(access, MAP_CLASSIFICATION_GUIDE_METADATA, + AccessStatus.ALLOWED); + check(access, MAP_EMAIL_METADATA, AccessStatus.ALLOWED); + check(access, MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, PASSWORD_CONTROL, AccessStatus.ALLOWED); + check(access, PLANNING_REVIEW_CYCLES, + AccessStatus.DENIED); + check(access, RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, SELECT_AUDIT_METADATA, + AccessStatus.ALLOWED); + check(access, TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, UNFREEZE, AccessStatus.DENIED); + check(access, UPDATE_CLASSIFICATION_DATES, + AccessStatus.ALLOWED); + check(access, UPDATE_EXEMPTION_CATEGORIES, + AccessStatus.ALLOWED); + check(access, UPDATE_TRIGGER_DATES, + AccessStatus.ALLOWED); + check(access, UPDATE_VITAL_RECORD_CYCLE_INFORMATION, + AccessStatus.ALLOWED); + check(access, UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, + AccessStatus.ALLOWED); + check(access, VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, VIEW_UPDATE_REASONS_FOR_FREEZE, + AccessStatus.DENIED); + + return null; + } + }, false, true); + } + + /** + * Test record as admin + */ + public void testRecordAsAdmin() + { + retryingTransactionHelper.doInTransaction( + new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil + .setFullyAuthenticatedUser(AuthenticationUtil + .getAdminUserName()); + Map access = securityService + .getCapabilities(record); + assertEquals(65, access.size()); + check(access, ACCESS_AUDIT, AccessStatus.ALLOWED); + check(access, ADD_MODIFY_EVENT_DATES, + AccessStatus.DENIED); + check(access, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + AccessStatus.DENIED); + check(access, ATTACH_RULES_TO_METADATA_PROPERTIES, + AccessStatus.ALLOWED); + check(access, AUTHORIZE_ALL_TRANSFERS, + AccessStatus.DENIED); + check(access, AUTHORIZE_NOMINATED_TRANSFERS, + AccessStatus.DENIED); + check(access, CHANGE_OR_DELETE_REFERENCES, + AccessStatus.UNDETERMINED); + check(access, CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, CREATE_AND_ASSOCIATE_SELECTION_LISTS, + AccessStatus.ALLOWED); + check(access, + CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_EVENTS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_FOLDERS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_RECORD_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_REFERENCE_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_ROLES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_TIMEFRAMES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, + AccessStatus.DENIED); + check(access, CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_AUDIT_AS_RECORD, + AccessStatus.ALLOWED); + check(access, DECLARE_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_RECORDS_IN_CLOSED_FOLDERS, + AccessStatus.DENIED); + check(access, DELETE_AUDIT, AccessStatus.ALLOWED); + check(access, DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, DELETE_RECORDS, AccessStatus.ALLOWED); + check(access, DESTROY_RECORDS, AccessStatus.DENIED); + check(access, + DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, + AccessStatus.DENIED); + check(access, DISPLAY_RIGHTS_REPORT, + AccessStatus.ALLOWED); + check(access, EDIT_DECLARED_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_NON_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_RECORD_METADATA, + AccessStatus.ALLOWED); + check(access, EDIT_SELECTION_LISTS, + AccessStatus.ALLOWED); + check(access, ENABLE_DISABLE_AUDIT_BY_TYPES, + AccessStatus.ALLOWED); + check(access, EXPORT_AUDIT, AccessStatus.ALLOWED); + check(access, EXTEND_RETENTION_PERIOD_OR_FREEZE, + AccessStatus.ALLOWED); + check(access, MAKE_OPTIONAL_PARAMETERS_MANDATORY, + AccessStatus.ALLOWED); + check(access, MANAGE_ACCESS_CONTROLS, + AccessStatus.ALLOWED); + check(access, MANAGE_ACCESS_RIGHTS, + AccessStatus.ALLOWED); + check(access, MANUALLY_CHANGE_DISPOSITION_DATES, + AccessStatus.DENIED); + check(access, MAP_CLASSIFICATION_GUIDE_METADATA, + AccessStatus.ALLOWED); + check(access, MAP_EMAIL_METADATA, AccessStatus.ALLOWED); + check(access, MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, PASSWORD_CONTROL, AccessStatus.ALLOWED); + check(access, PLANNING_REVIEW_CYCLES, + AccessStatus.DENIED); + check(access, RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, SELECT_AUDIT_METADATA, + AccessStatus.ALLOWED); + check(access, TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, UNFREEZE, AccessStatus.DENIED); + check(access, UPDATE_CLASSIFICATION_DATES, + AccessStatus.ALLOWED); + check(access, UPDATE_EXEMPTION_CATEGORIES, + AccessStatus.ALLOWED); + check(access, UPDATE_TRIGGER_DATES, + AccessStatus.ALLOWED); + check(access, UPDATE_VITAL_RECORD_CYCLE_INFORMATION, + AccessStatus.ALLOWED); + check(access, UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, + AccessStatus.ALLOWED); + check(access, VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, VIEW_UPDATE_REASONS_FOR_FREEZE, + AccessStatus.DENIED); + + return null; + } + }, false, true); + } + + /** + * Test record as administrator + */ + public void testRecordAsAdministrator() + { + retryingTransactionHelper.doInTransaction( + new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil + .setFullyAuthenticatedUser(rmAdminName); + Map access = securityService + .getCapabilities(record); + assertEquals(65, access.size()); + check(access, ACCESS_AUDIT, AccessStatus.ALLOWED); + check(access, ADD_MODIFY_EVENT_DATES, + AccessStatus.DENIED); + check(access, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + AccessStatus.DENIED); + check(access, ATTACH_RULES_TO_METADATA_PROPERTIES, + AccessStatus.ALLOWED); + check(access, AUTHORIZE_ALL_TRANSFERS, + AccessStatus.DENIED); + check(access, AUTHORIZE_NOMINATED_TRANSFERS, + AccessStatus.DENIED); + check(access, CHANGE_OR_DELETE_REFERENCES, + AccessStatus.UNDETERMINED); + check(access, CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, CREATE_AND_ASSOCIATE_SELECTION_LISTS, + AccessStatus.ALLOWED); + check(access, + CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_EVENTS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_FOLDERS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_RECORD_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_REFERENCE_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_ROLES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_TIMEFRAMES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, + AccessStatus.DENIED); + check(access, CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_AUDIT_AS_RECORD, + AccessStatus.ALLOWED); + check(access, DECLARE_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_RECORDS_IN_CLOSED_FOLDERS, + AccessStatus.DENIED); + check(access, DELETE_AUDIT, AccessStatus.ALLOWED); + check(access, DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, DELETE_RECORDS, AccessStatus.ALLOWED); + check(access, DESTROY_RECORDS, AccessStatus.DENIED); + check(access, + DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, + AccessStatus.DENIED); + check(access, DISPLAY_RIGHTS_REPORT, + AccessStatus.ALLOWED); + check(access, EDIT_DECLARED_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_NON_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_RECORD_METADATA, + AccessStatus.ALLOWED); + check(access, EDIT_SELECTION_LISTS, + AccessStatus.ALLOWED); + check(access, ENABLE_DISABLE_AUDIT_BY_TYPES, + AccessStatus.ALLOWED); + check(access, EXPORT_AUDIT, AccessStatus.ALLOWED); + check(access, EXTEND_RETENTION_PERIOD_OR_FREEZE, + AccessStatus.ALLOWED); + check(access, MAKE_OPTIONAL_PARAMETERS_MANDATORY, + AccessStatus.ALLOWED); + check(access, MANAGE_ACCESS_CONTROLS, + AccessStatus.ALLOWED); + check(access, MANAGE_ACCESS_RIGHTS, + AccessStatus.ALLOWED); + check(access, MANUALLY_CHANGE_DISPOSITION_DATES, + AccessStatus.DENIED); + check(access, MAP_CLASSIFICATION_GUIDE_METADATA, + AccessStatus.ALLOWED); + check(access, MAP_EMAIL_METADATA, AccessStatus.ALLOWED); + check(access, MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, PASSWORD_CONTROL, AccessStatus.ALLOWED); + check(access, PLANNING_REVIEW_CYCLES, + AccessStatus.DENIED); + check(access, RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, SELECT_AUDIT_METADATA, + AccessStatus.ALLOWED); + check(access, TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, UNFREEZE, AccessStatus.DENIED); + check(access, UPDATE_CLASSIFICATION_DATES, + AccessStatus.ALLOWED); + check(access, UPDATE_EXEMPTION_CATEGORIES, + AccessStatus.ALLOWED); + check(access, UPDATE_TRIGGER_DATES, + AccessStatus.ALLOWED); + check(access, UPDATE_VITAL_RECORD_CYCLE_INFORMATION, + AccessStatus.ALLOWED); + check(access, UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, + AccessStatus.ALLOWED); + check(access, VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, VIEW_UPDATE_REASONS_FOR_FREEZE, + AccessStatus.DENIED); + + return null; + } + }, false, true); + } + + /** + * Test record as records manager + */ + public void testRecordAsRecordsManager() + { + retryingTransactionHelper.doInTransaction( + new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil + .setFullyAuthenticatedUser(recordsManagerName); + // setFilingOnRecord(record, recordsManagerName); + Map access = securityService + .getCapabilities(record); + assertEquals(65, access.size()); // 58 + File + check(access, ACCESS_AUDIT, AccessStatus.ALLOWED); + check(access, ADD_MODIFY_EVENT_DATES, + AccessStatus.DENIED); + check(access, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + AccessStatus.DENIED); + check(access, ATTACH_RULES_TO_METADATA_PROPERTIES, + AccessStatus.ALLOWED); + check(access, AUTHORIZE_ALL_TRANSFERS, + AccessStatus.DENIED); + check(access, AUTHORIZE_NOMINATED_TRANSFERS, + AccessStatus.DENIED); + check(access, CHANGE_OR_DELETE_REFERENCES, + AccessStatus.UNDETERMINED); + check(access, CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, CREATE_AND_ASSOCIATE_SELECTION_LISTS, + AccessStatus.ALLOWED); + check(access, + CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_EVENTS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_FOLDERS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_RECORD_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_REFERENCE_TYPES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_ROLES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_TIMEFRAMES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, + AccessStatus.DENIED); + check(access, CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_AUDIT_AS_RECORD, + AccessStatus.ALLOWED); + check(access, DECLARE_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_RECORDS_IN_CLOSED_FOLDERS, + AccessStatus.DENIED); + check(access, DELETE_AUDIT, AccessStatus.ALLOWED); + check(access, DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, DELETE_RECORDS, AccessStatus.ALLOWED); + check(access, DESTROY_RECORDS, AccessStatus.DENIED); + check(access, + DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, + AccessStatus.DENIED); + check(access, DISPLAY_RIGHTS_REPORT, + AccessStatus.ALLOWED); + check(access, EDIT_DECLARED_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_NON_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_RECORD_METADATA, + AccessStatus.ALLOWED); + check(access, EDIT_SELECTION_LISTS, + AccessStatus.ALLOWED); + check(access, ENABLE_DISABLE_AUDIT_BY_TYPES, + AccessStatus.ALLOWED); + check(access, EXPORT_AUDIT, AccessStatus.ALLOWED); + check(access, EXTEND_RETENTION_PERIOD_OR_FREEZE, + AccessStatus.ALLOWED); + check(access, MAKE_OPTIONAL_PARAMETERS_MANDATORY, + AccessStatus.ALLOWED); + check(access, MANAGE_ACCESS_CONTROLS, + AccessStatus.DENIED); + check(access, MANAGE_ACCESS_RIGHTS, + AccessStatus.ALLOWED); + check(access, MANUALLY_CHANGE_DISPOSITION_DATES, + AccessStatus.DENIED); + check(access, MAP_CLASSIFICATION_GUIDE_METADATA, + AccessStatus.ALLOWED); + check(access, MAP_EMAIL_METADATA, AccessStatus.ALLOWED); + check(access, MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, PASSWORD_CONTROL, AccessStatus.ALLOWED); + check(access, PLANNING_REVIEW_CYCLES, + AccessStatus.DENIED); + check(access, RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, SELECT_AUDIT_METADATA, + AccessStatus.ALLOWED); + check(access, TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, UNFREEZE, AccessStatus.DENIED); + check(access, UPDATE_CLASSIFICATION_DATES, + AccessStatus.ALLOWED); + check(access, UPDATE_EXEMPTION_CATEGORIES, + AccessStatus.ALLOWED); + check(access, UPDATE_TRIGGER_DATES, + AccessStatus.ALLOWED); + check(access, UPDATE_VITAL_RECORD_CYCLE_INFORMATION, + AccessStatus.ALLOWED); + check(access, UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, + AccessStatus.ALLOWED); + check(access, VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, VIEW_UPDATE_REASONS_FOR_FREEZE, + AccessStatus.DENIED); + + return null; + } + }, false, true); + } + + /** + * Test record as security officer + */ + public void testRecordAsSecurityOfficer() + { + retryingTransactionHelper.doInTransaction( + new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil + .setFullyAuthenticatedUser(securityOfficerName); + // setFilingOnRecord(record, securityOfficerName); + Map access = securityService + .getCapabilities(record); + assertEquals(65, access.size()); // 58 + File + check(access, ACCESS_AUDIT, AccessStatus.DENIED); + check(access, ADD_MODIFY_EVENT_DATES, + AccessStatus.DENIED); + check(access, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + AccessStatus.DENIED); + check(access, ATTACH_RULES_TO_METADATA_PROPERTIES, + AccessStatus.DENIED); + check(access, AUTHORIZE_ALL_TRANSFERS, + AccessStatus.DENIED); + check(access, AUTHORIZE_NOMINATED_TRANSFERS, + AccessStatus.DENIED); + check(access, CHANGE_OR_DELETE_REFERENCES, + AccessStatus.DENIED); + check(access, CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, CREATE_AND_ASSOCIATE_SELECTION_LISTS, + AccessStatus.DENIED); + check(access, + CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_EVENTS, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FOLDERS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_RECORD_TYPES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_REFERENCE_TYPES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_ROLES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_TIMEFRAMES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, + AccessStatus.DENIED); + check(access, CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_AUDIT_AS_RECORD, + AccessStatus.DENIED); + check(access, DECLARE_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_RECORDS_IN_CLOSED_FOLDERS, + AccessStatus.DENIED); + check(access, DELETE_AUDIT, AccessStatus.DENIED); + check(access, DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, DELETE_RECORDS, AccessStatus.DENIED); + check(access, DESTROY_RECORDS, AccessStatus.DENIED); + check(access, + DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, + AccessStatus.DENIED); + check(access, DISPLAY_RIGHTS_REPORT, + AccessStatus.DENIED); + check(access, EDIT_DECLARED_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_NON_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_RECORD_METADATA, + AccessStatus.ALLOWED); + check(access, EDIT_SELECTION_LISTS, AccessStatus.DENIED); + check(access, ENABLE_DISABLE_AUDIT_BY_TYPES, + AccessStatus.DENIED); + check(access, EXPORT_AUDIT, AccessStatus.DENIED); + check(access, EXTEND_RETENTION_PERIOD_OR_FREEZE, + AccessStatus.DENIED); + check(access, MAKE_OPTIONAL_PARAMETERS_MANDATORY, + AccessStatus.DENIED); + check(access, MANAGE_ACCESS_CONTROLS, + AccessStatus.DENIED); + check(access, MANAGE_ACCESS_RIGHTS, AccessStatus.DENIED); + check(access, MANUALLY_CHANGE_DISPOSITION_DATES, + AccessStatus.DENIED); + check(access, MAP_CLASSIFICATION_GUIDE_METADATA, + AccessStatus.DENIED); + check(access, MAP_EMAIL_METADATA, AccessStatus.DENIED); + check(access, MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, PASSWORD_CONTROL, AccessStatus.DENIED); + check(access, PLANNING_REVIEW_CYCLES, + AccessStatus.DENIED); + check(access, RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, SELECT_AUDIT_METADATA, + AccessStatus.DENIED); + check(access, TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, UNFREEZE, AccessStatus.DENIED); + check(access, UPDATE_CLASSIFICATION_DATES, + AccessStatus.ALLOWED); + check(access, UPDATE_EXEMPTION_CATEGORIES, + AccessStatus.ALLOWED); + check(access, UPDATE_TRIGGER_DATES, AccessStatus.DENIED); + check(access, UPDATE_VITAL_RECORD_CYCLE_INFORMATION, + AccessStatus.DENIED); + check(access, UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, + AccessStatus.ALLOWED); + check(access, VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, VIEW_UPDATE_REASONS_FOR_FREEZE, + AccessStatus.DENIED); + + return null; + } + }, false, true); + } + + /** + * Test records as power user + */ + public void testRecordAsPowerUser() + { + retryingTransactionHelper.doInTransaction( + new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + + AuthenticationUtil + .setFullyAuthenticatedUser(powerUserName); + // setFilingOnRecord(record, powerUserName); + Map access = securityService + .getCapabilities(record); + assertEquals(65, access.size()); // 58 + File + check(access, ACCESS_AUDIT, AccessStatus.DENIED); + check(access, ADD_MODIFY_EVENT_DATES, + AccessStatus.DENIED); + check(access, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + AccessStatus.DENIED); + check(access, ATTACH_RULES_TO_METADATA_PROPERTIES, + AccessStatus.DENIED); + check(access, AUTHORIZE_ALL_TRANSFERS, + AccessStatus.DENIED); + check(access, AUTHORIZE_NOMINATED_TRANSFERS, + AccessStatus.DENIED); + check(access, CHANGE_OR_DELETE_REFERENCES, + AccessStatus.DENIED); + check(access, CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, CREATE_AND_ASSOCIATE_SELECTION_LISTS, + AccessStatus.DENIED); + check(access, + CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_EVENTS, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FOLDERS, + AccessStatus.ALLOWED); + check(access, CREATE_MODIFY_DESTROY_RECORD_TYPES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_REFERENCE_TYPES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_ROLES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_TIMEFRAMES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, + AccessStatus.DENIED); + check(access, CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_AUDIT_AS_RECORD, + AccessStatus.DENIED); + check(access, DECLARE_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_RECORDS_IN_CLOSED_FOLDERS, + AccessStatus.DENIED); + check(access, DELETE_AUDIT, AccessStatus.DENIED); + check(access, DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, DELETE_RECORDS, AccessStatus.DENIED); + check(access, DESTROY_RECORDS, AccessStatus.DENIED); + check(access, + DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, + AccessStatus.DENIED); + check(access, DISPLAY_RIGHTS_REPORT, + AccessStatus.DENIED); + check(access, EDIT_DECLARED_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_NON_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_RECORD_METADATA, + AccessStatus.ALLOWED); + check(access, EDIT_SELECTION_LISTS, AccessStatus.DENIED); + check(access, ENABLE_DISABLE_AUDIT_BY_TYPES, + AccessStatus.DENIED); + check(access, EXPORT_AUDIT, AccessStatus.DENIED); + check(access, EXTEND_RETENTION_PERIOD_OR_FREEZE, + AccessStatus.DENIED); + check(access, MAKE_OPTIONAL_PARAMETERS_MANDATORY, + AccessStatus.DENIED); + check(access, MANAGE_ACCESS_CONTROLS, + AccessStatus.DENIED); + check(access, MANAGE_ACCESS_RIGHTS, AccessStatus.DENIED); + check(access, MANUALLY_CHANGE_DISPOSITION_DATES, + AccessStatus.DENIED); + check(access, MAP_CLASSIFICATION_GUIDE_METADATA, + AccessStatus.DENIED); + check(access, MAP_EMAIL_METADATA, AccessStatus.DENIED); + check(access, MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, PASSWORD_CONTROL, AccessStatus.DENIED); + check(access, PLANNING_REVIEW_CYCLES, + AccessStatus.DENIED); + check(access, RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, SELECT_AUDIT_METADATA, + AccessStatus.DENIED); + check(access, TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, UNFREEZE, AccessStatus.DENIED); + check(access, UPDATE_CLASSIFICATION_DATES, + AccessStatus.DENIED); + check(access, UPDATE_EXEMPTION_CATEGORIES, + AccessStatus.DENIED); + check(access, UPDATE_TRIGGER_DATES, AccessStatus.DENIED); + check(access, UPDATE_VITAL_RECORD_CYCLE_INFORMATION, + AccessStatus.DENIED); + check(access, UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, + AccessStatus.DENIED); + check(access, VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, VIEW_UPDATE_REASONS_FOR_FREEZE, + AccessStatus.DENIED); + + return null; + } + }, false, true); + } + + /** + * Test record as user + */ + public void testRecordAsUser() + { + retryingTransactionHelper.doInTransaction( + new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil + .setFullyAuthenticatedUser(rmUserName); + // setFilingOnRecord(record, rmUserName); + Map access = securityService + .getCapabilities(record); + assertEquals(65, access.size()); // 58 + File + check(access, ACCESS_AUDIT, AccessStatus.DENIED); + check(access, ADD_MODIFY_EVENT_DATES, + AccessStatus.DENIED); + check(access, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + AccessStatus.DENIED); + check(access, ATTACH_RULES_TO_METADATA_PROPERTIES, + AccessStatus.DENIED); + check(access, AUTHORIZE_ALL_TRANSFERS, + AccessStatus.DENIED); + check(access, AUTHORIZE_NOMINATED_TRANSFERS, + AccessStatus.DENIED); + check(access, CHANGE_OR_DELETE_REFERENCES, + AccessStatus.DENIED); + check(access, CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, CREATE_AND_ASSOCIATE_SELECTION_LISTS, + AccessStatus.DENIED); + check(access, + CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_EVENTS, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_FOLDERS, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_RECORD_TYPES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_REFERENCE_TYPES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_ROLES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_TIMEFRAMES, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, + AccessStatus.DENIED); + check(access, CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, + AccessStatus.DENIED); + check(access, CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_AUDIT_AS_RECORD, + AccessStatus.DENIED); + check(access, DECLARE_RECORDS, AccessStatus.DENIED); + check(access, DECLARE_RECORDS_IN_CLOSED_FOLDERS, + AccessStatus.DENIED); + check(access, DELETE_AUDIT, AccessStatus.DENIED); + check(access, DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, DELETE_RECORDS, AccessStatus.DENIED); + check(access, DESTROY_RECORDS, AccessStatus.DENIED); + check(access, + DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, + AccessStatus.DENIED); + check(access, DISPLAY_RIGHTS_REPORT, + AccessStatus.DENIED); + check(access, EDIT_DECLARED_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_NON_RECORD_METADATA, + AccessStatus.DENIED); + check(access, EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, EDIT_SELECTION_LISTS, AccessStatus.DENIED); + check(access, ENABLE_DISABLE_AUDIT_BY_TYPES, + AccessStatus.DENIED); + check(access, EXPORT_AUDIT, AccessStatus.DENIED); + check(access, EXTEND_RETENTION_PERIOD_OR_FREEZE, + AccessStatus.DENIED); + check(access, MAKE_OPTIONAL_PARAMETERS_MANDATORY, + AccessStatus.DENIED); + check(access, MANAGE_ACCESS_CONTROLS, + AccessStatus.DENIED); + check(access, MANAGE_ACCESS_RIGHTS, AccessStatus.DENIED); + check(access, MANUALLY_CHANGE_DISPOSITION_DATES, + AccessStatus.DENIED); + check(access, MAP_CLASSIFICATION_GUIDE_METADATA, + AccessStatus.DENIED); + check(access, MAP_EMAIL_METADATA, AccessStatus.DENIED); + check(access, MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, PASSWORD_CONTROL, AccessStatus.DENIED); + check(access, PLANNING_REVIEW_CYCLES, + AccessStatus.DENIED); + check(access, RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, SELECT_AUDIT_METADATA, + AccessStatus.DENIED); + check(access, TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, UNFREEZE, AccessStatus.DENIED); + check(access, UPDATE_CLASSIFICATION_DATES, + AccessStatus.DENIED); + check(access, UPDATE_EXEMPTION_CATEGORIES, + AccessStatus.DENIED); + check(access, UPDATE_TRIGGER_DATES, AccessStatus.DENIED); + check(access, UPDATE_VITAL_RECORD_CYCLE_INFORMATION, + AccessStatus.DENIED); + check(access, UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, + AccessStatus.DENIED); + check(access, VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, VIEW_UPDATE_REASONS_FOR_FREEZE, + AccessStatus.DENIED); + + return null; + } + }, false, true); + } + + // private void setFilingOnRecord(NodeRef record, String authority) + // { + // NodeRef recordFolder = + // nodeService.getPrimaryParent(record).getParentRef(); + // permissionService.setPermission(recordFolder, authority, FILING, true); + // permissionService.setPermission(nodeService.getPrimaryParent(recordFolder).getParentRef(), + // authority, READ_RECORDS, true); + // } + // + // private void setFilingOnRecordFolder(NodeRef recordFolder, String + // authority) + // { + // permissionService.setPermission(recordFolder, authority, FILING, true); + // permissionService.setPermission(nodeService.getPrimaryParent(recordFolder).getParentRef(), + // authority, READ_RECORDS, true); + // } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/DeclarativeCapabilityTest.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/DeclarativeCapabilityTest.java new file mode 100644 index 0000000000..27c65e35b4 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/DeclarativeCapabilityTest.java @@ -0,0 +1,245 @@ +/* + * 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.module.org_alfresco_module_rm.test.capabilities; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.FilePlanComponentKind; +import org.alfresco.module.org_alfresco_module_rm.capability.Capability; +import org.alfresco.module.org_alfresco_module_rm.capability.RMPermissionModel; +import org.alfresco.module.org_alfresco_module_rm.capability.declarative.CapabilityCondition; +import org.alfresco.module.org_alfresco_module_rm.capability.declarative.DeclarativeCapability; +import org.alfresco.module.org_alfresco_module_rm.security.Role; +import org.alfresco.module.org_alfresco_module_rm.test.util.BaseRMTestCase; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.AccessStatus; + +/** + * Declarative capability unit test + * + * @author Roy Wetherall + */ +public class DeclarativeCapabilityTest extends BaseRMTestCase +{ + private NodeRef record; + private NodeRef declaredRecord; + + private NodeRef recordFolderContainsFrozen; + private NodeRef frozenRecord; + private NodeRef frozenRecord2; + private NodeRef frozenRecordFolder; + + @Override + protected boolean isUserTest() + { + return true; + } + + @Override + protected void setupTestDataImpl() + { + super.setupTestDataImpl(); + + // Pre-filed content + record = createRecord(rmFolder, "record.txt"); + declaredRecord = createRecord(rmFolder, "declaredRecord.txt"); + + + // Open folder + // Closed folder + + recordFolderContainsFrozen = rmService.createRecordFolder(rmContainer, "containsFrozen"); + frozenRecord = createRecord(rmFolder, "frozenRecord.txt"); + frozenRecord2 = createRecord(recordFolderContainsFrozen, "frozen2.txt"); + frozenRecordFolder = rmService.createRecordFolder(rmContainer, "frozenRecordFolder"); + + } + + @Override + protected void setupTestData() + { + super.setupTestData(); + + retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + + declareRecord(declaredRecord); + declareRecord(frozenRecord); + declareRecord(frozenRecord2); + freeze(frozenRecord); + freeze(frozenRecordFolder); + freeze(frozenRecord2); + + return null; + } + }); + } + + @Override + protected void tearDownImpl() + { + // Unfreeze stuff so it can be deleted + unfreeze(frozenRecord); + unfreeze(frozenRecordFolder); + unfreeze(frozenRecord2); + + super.tearDownImpl(); + } + + @Override + protected void setupTestUsersImpl(NodeRef filePlan) + { + super.setupTestUsersImpl(filePlan); + + // Give all the users file permission objects + for (String user : testUsers) + { + securityService.setPermission(rmFolder, user, RMPermissionModel.FILING); + } + } + + public void testDeclarativeCapabilities() + { + Set capabilities = capabilityService.getCapabilities(); + for (Capability capability : capabilities) + { + if (capability instanceof DeclarativeCapability && + capability.isGroupCapability() == false && + capability.getName().equals("MoveRecords") == false && + capability.getName().equals("DeleteLinks") == false && + capability.getName().equals("ChangeOrDeleteReferences") == false && + capability.getActionNames().isEmpty() == true) + { + testDeclarativeCapability((DeclarativeCapability)capability); + } + } + } + + private void testDeclarativeCapability(final DeclarativeCapability capability) + { + for (String user : testUsers) + { + testDeclarativeCapability(capability, user, filePlan); + testDeclarativeCapability(capability, user, rmContainer); + testDeclarativeCapability(capability, user, rmFolder); + testDeclarativeCapability(capability, user, record); + } + } + + private void testDeclarativeCapability(final DeclarativeCapability capability, final String userName, final NodeRef filePlanComponent) + { + doTestInTransaction(new Test() + { + @Override + public Void run() + { + AccessStatus accessStatus = capability.hasPermission(filePlanComponent); + + Set roles = securityService.getRolesByUser(filePlan, userName); + if (roles.isEmpty() == true) + { + assertEquals("User " + userName + " has no RM role so we expect access to be denied for capability " + capability.getName(), + AccessStatus.DENIED, + accessStatus); + } + else + { + // Do the kind check here ... + FilePlanComponentKind actualKind = rmService.getFilePlanComponentKind(filePlanComponent); + List kinds = capability.getKinds(); + + if (kinds == null || + kinds.contains(actualKind.toString()) == true) + { + Map conditions = capability.getConditions(); + boolean conditionResult = getConditionResult(filePlanComponent, conditions); + + assertEquals("User is expected to only have one role.", 1, roles.size()); + Role role = new ArrayList(roles).get(0); + assertNotNull(role); + + Set roleCapabilities = role.getCapabilities(); + if (roleCapabilities.contains(capability.getName()) == true && conditionResult == true) + { + assertEquals("User " + userName + " has the role " + role.getDisplayLabel() + + " so we expect access to be allowed for capability " + capability.getName() + " on the object " + + (String)nodeService.getProperty(filePlanComponent, ContentModel.PROP_NAME), + AccessStatus.ALLOWED, + accessStatus); + } + else + { + assertEquals("User " + userName + " has the role " + role.getDisplayLabel() + " so we expect access to be denied for capability " + capability.getName(), + AccessStatus.DENIED, + accessStatus); + } + } + else + { + // Expect fail since the kind is not expected by the capability + assertEquals("NodeRef is of kind" + actualKind + " so we expect access to be denied for capability " + capability.getName(), + AccessStatus.DENIED, + accessStatus); + } + } + + return null; + } + }, userName); + } + + private boolean getConditionResult(NodeRef nodeRef, Map conditions) + { + boolean result = true; + + if (conditions != null && conditions.size() != 0) + { + for (Map.Entry entry : conditions.entrySet()) + { + // Get the condition bean + CapabilityCondition condition = (CapabilityCondition)applicationContext.getBean(entry.getKey()); + assertNotNull("Invalid condition name.", condition); + + boolean actual = condition.evaluate(nodeRef); + if (actual != entry.getValue().booleanValue()) + { + result = false; + break; + } + } + } + + return result; + } + + public void testFrozenCondition() + { + + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/jscript/CapabilitiesTest.js b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/jscript/CapabilitiesTest.js new file mode 100644 index 0000000000..fe05c9a9d6 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/jscript/CapabilitiesTest.js @@ -0,0 +1,18 @@ +function main() +{ + test.assertNotNull(filePlan); + test.assertNotNull(record); + + var rmNode = rmService.getRecordsManagementNode(record); + test.assertNotNull(rmNode); + + var capabilities = rmNode.capabilities; + var countCheck = capabilities.length != 0; + test.assertTrue(countCheck); + + var capability = capabilities[0]; + test.assertNotNull(capability); + test.assertNotNull(capability.name); +} + +main(); \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/jscript/JSONConversionComponentTest.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/jscript/JSONConversionComponentTest.java new file mode 100644 index 0000000000..9569e77008 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/jscript/JSONConversionComponentTest.java @@ -0,0 +1,159 @@ +/* + * 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.module.org_alfresco_module_rm.test.jscript; + +import java.io.Serializable; + +import org.alfresco.module.org_alfresco_module_rm.test.util.BaseRMTestCase; +import org.alfresco.repo.jscript.app.JSONConversionComponent; +import org.alfresco.service.cmr.repository.NodeRef; +import org.apache.commons.lang.ArrayUtils; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * @author Roy Wetherall + */ +public class JSONConversionComponentTest extends BaseRMTestCase +{ + private JSONConversionComponent converter; + + private NodeRef record; + + @Override + protected void initServices() + { + super.initServices(); + converter = (JSONConversionComponent)applicationContext.getBean("jsonConversionComponent"); + } + + @Override + protected void setupTestDataImpl() + { + super.setupTestDataImpl(); + + // Create records + record = createRecord(rmFolder, "testRecord.txt"); + } + + public void testJSON() throws Exception + { + doTestInTransaction(new JSONTest + ( + filePlan, + new String[]{"isRmNode", "true", "boolean"}, + new String[]{"rmNode.kind", "FILE_PLAN"} + ){}); + + doTestInTransaction(new JSONTest + ( + rmContainer, + new String[]{"isRmNode", "true", "boolean"}, + new String[]{"rmNode.kind", "RECORD_CATEGORY"} + ){}); + + doTestInTransaction(new JSONTest + ( + rmFolder, + new String[]{"isRmNode", "true", "boolean"}, + new String[]{"rmNode.kind", "RECORD_FOLDER"}, + new String[]{"rmNode.closed", "false", "boolean"}, + new String[]{"rmNode.declared", "false", "boolean"}, + new String[]{"rmNode.frozen", "false", "boolean"}, + new String[]{"rmNode.metatdata-stub", "false", "boolean"} + //containsFrozen + ){}); + + doTestInTransaction(new JSONTest + ( + record, + new String[]{"isRmNode", "true", "boolean"}, + new String[]{"rmNode.kind", "RECORD"}, + new String[]{"rmNode.declared", "false", "boolean"}, + new String[]{"rmNode.frozen", "false", "boolean"}, + new String[]{"rmNode.metatdata-stub", "false", "boolean"} + ){}); + } + + class JSONTest extends Test + { + private NodeRef nodeRef; + private String[][] testValues; + + public JSONTest(NodeRef nodeRef, String[] ... testValues) + { + this.nodeRef = nodeRef; + this.testValues = testValues; + } + + @Override + public JSONObject run() throws Exception + { + String json = converter.toJSON(nodeRef, true); + System.out.println(json); + return new JSONObject(json); + } + + @Override + public void test(JSONObject result) throws Exception + { + for (String[] testValue : testValues) + { + String key = testValue[0]; + String type = "string"; + if (testValue.length == 3) + { + type = testValue[2]; + } + Serializable value = convertValue(testValue[1], type); + Serializable actualValue = (Serializable)getValue(result, key); + + assertEquals("The key " + key + " did not have the expected value.", value, actualValue); + } + } + + private Serializable convertValue(String stringValue, String type) + { + Serializable value = stringValue; + if (type.equals("boolean") == true) + { + value = new Boolean(stringValue); + } + return value; + } + + private Object getValue(JSONObject jsonObject, String key) throws JSONException + { + return getValue(jsonObject, key.split("\\.")); + } + + private Object getValue(JSONObject jsonObject, String[] key) throws JSONException + { + if (key.length == 1) + { + return jsonObject.get(key[0]); + } + else + { + return getValue(jsonObject.getJSONObject(key[0]), + (String[])ArrayUtils.subarray(key, 1, key.length)); + } + } + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/jscript/RMJScriptTest.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/jscript/RMJScriptTest.java new file mode 100644 index 0000000000..1f9fe06d7a --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/jscript/RMJScriptTest.java @@ -0,0 +1,78 @@ +/* + * 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.module.org_alfresco_module_rm.test.jscript; + +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.test.util.BaseRMTestCase; +import org.alfresco.repo.jscript.ClasspathScriptLocation; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.ScriptLocation; +import org.alfresco.service.cmr.repository.ScriptService; + +/** + * @author Roy Wetherall + */ +public class RMJScriptTest extends BaseRMTestCase +{ + private static String SCRIPT_PATH = "org/alfresco/module/org_alfresco_module_rm/test/jscript/"; + private static String CAPABILITIES_TEST = "CapabilitiesTest.js"; + + private ScriptService scriptService; + + @Override + protected void initServices() + { + this.scriptService = (ScriptService)this.applicationContext.getBean("ScriptService"); + } + + public void testCapabilities() throws Exception + { + doTestInTransaction(new Test() + { + @Override + public NodeRef run() + { + NodeRef record = createRecord(rmFolder, "testRecord.txt"); + declareRecord(record); + return record; + } + + @Override + public void test(NodeRef record) throws Exception + { + + // Create a model to pass to the unit test scripts + Map model = new HashMap(1); + model.put("filePlan", filePlan); + model.put("record", record); + + executeScript(CAPABILITIES_TEST, model); + } + }); + } + + private void executeScript(String script, Map model) + { + // Execute the unit test script + ScriptLocation location = new ClasspathScriptLocation(SCRIPT_PATH + script); + this.scriptService.executeScript(location, model); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/DispositionServiceImplTest.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/DispositionServiceImplTest.java new file mode 100644 index 0000000000..a81f7594e1 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/DispositionServiceImplTest.java @@ -0,0 +1,800 @@ +/* + * 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.module.org_alfresco_module_rm.test.service; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionAction; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionActionDefinition; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService; +import org.alfresco.module.org_alfresco_module_rm.event.EventCompletionDetails; +import org.alfresco.module.org_alfresco_module_rm.job.publish.PublishExecutor; +import org.alfresco.module.org_alfresco_module_rm.job.publish.PublishExecutorRegistry; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.module.org_alfresco_module_rm.test.util.BaseRMTestCase; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + +/** + * Disposition service implementation unit test. + * + * @author Roy Wetherall + */ +public class DispositionServiceImplTest extends BaseRMTestCase +{ + @Override + protected boolean isMultiHierarchyTest() + { + return true; + } + + /** + * @see DispositionService#getDispositionSchedule(org.alfresco.service.cmr.repository.NodeRef) + */ + public void testGetDispositionSchedule() throws Exception + { + doTestInTransaction(new Test() + { + @Override + public Void run() + { + // Check for null lookup's + assertNull(dispositionService.getDispositionSchedule(filePlan)); + + // Get the containers disposition schedule + DispositionSchedule ds = dispositionService.getDispositionSchedule(rmContainer); + assertNotNull(ds); + checkDispositionSchedule(ds, false); + + // Get the folders disposition schedule + ds = dispositionService.getDispositionSchedule(rmContainer); + assertNotNull(ds); + checkDispositionSchedule(ds, false); + + return null; + } + + }); + + // Failure: Root node + doTestInTransaction(new FailureTest + ( + "Should not be able to get adisposition schedule for the root node", + AlfrescoRuntimeException.class + ) + { + @Override + public void run() + { + dispositionService.getDispositionSchedule(rootNodeRef); + } + }); + + // Failure: Non-rm node + doTestInTransaction(new FailureTest() + { + @Override + public void run() + { + dispositionService.getDispositionSchedule(folder); + } + }); + } + + /** + * @see DispositionService#getDispositionSchedule(org.alfresco.service.cmr.repository.NodeRef) + */ + public void testGetDispositionScheduleMultiHier() throws Exception + { + doTestInTransaction(new Test() + { + @Override + public Void run() + { + assertNull(dispositionService.getDispositionSchedule(mhContainer)); + + // Level 1 + doCheck(mhContainer11, "ds11", false); + doCheck(mhContainer12, "ds12", false); + + // Level 2 + doCheck(mhContainer21, "ds11", false); + doCheck(mhContainer22, "ds12", false); + doCheck(mhContainer23, "ds23", false); + + // Level 3 + doCheck(mhContainer31, "ds11", false); + doCheck(mhContainer32, "ds12", false); + doCheck(mhContainer33, "ds33", true); + doCheck(mhContainer34, "ds23", false); + doCheck(mhContainer35, "ds35", true); + + // Folders + doCheckFolder(mhRecordFolder41, "ds11", false); + doCheckFolder(mhRecordFolder42, "ds12", false); + doCheckFolder(mhRecordFolder43, "ds33", true); + doCheckFolder(mhRecordFolder44, "ds23", false); + doCheckFolder(mhRecordFolder45, "ds35", true); + + return null; + } + + private void doCheck(NodeRef container, String dispositionInstructions, boolean isRecordLevel) + { + DispositionSchedule ds = dispositionService.getDispositionSchedule(container); + assertNotNull(ds); + checkDispositionSchedule(ds, dispositionInstructions, DEFAULT_DISPOSITION_AUTHORITY, isRecordLevel); + } + + private void doCheckFolder(NodeRef container, String dispositionInstructions, boolean isRecordLevel) + { + doCheck(container, dispositionInstructions, isRecordLevel); + if (isRecordLevel == false) + { + assertNotNull(dispositionService.getNextDispositionAction(container)); + } + } + }); + + } + + /** + * Checks a disposition schedule + * + * @param ds disposition scheduleS + */ + private void checkDispositionSchedule(DispositionSchedule ds, String dispositionInstructions, String dispositionAuthority, boolean isRecordLevel) + { + assertEquals(dispositionAuthority, ds.getDispositionAuthority()); + assertEquals(dispositionInstructions, ds.getDispositionInstructions()); + assertEquals(isRecordLevel, ds.isRecordLevelDisposition()); + + List defs = ds.getDispositionActionDefinitions(); + assertNotNull(defs); + assertEquals(2, defs.size()); + + DispositionActionDefinition defCutoff = ds.getDispositionActionDefinitionByName("cutoff"); + assertNotNull(defCutoff); + assertEquals("cutoff", defCutoff.getName()); + + DispositionActionDefinition defDestroy = ds.getDispositionActionDefinitionByName("destroy"); + assertNotNull(defDestroy); + assertEquals("destroy", defDestroy.getName()); + } + + /** + * + * @param ds + */ + private void checkDispositionSchedule(DispositionSchedule ds, boolean isRecordLevel) + { + checkDispositionSchedule(ds, DEFAULT_DISPOSITION_INSTRUCTIONS, DEFAULT_DISPOSITION_AUTHORITY, isRecordLevel); + } + + /** + * @see DispositionService#getAssociatedDispositionSchedule(NodeRef) + */ + public void testGetAssociatedDispositionSchedule() throws Exception + { + // Get associated disposition schedule for rmContainer + doTestInTransaction(new Test() + { + @Override + public Void run() + { + // Get the containers disposition schedule + DispositionSchedule ds = dispositionService.getAssociatedDispositionSchedule(rmContainer); + assertNotNull(ds); + checkDispositionSchedule(ds, false); + + // Show the null disposition schedules + assertNull(dispositionService.getAssociatedDispositionSchedule(filePlan)); + assertNull(dispositionService.getAssociatedDispositionSchedule(rmFolder)); + + return null; + } + }); + + // Failure: associated disposition schedule for non-rm node + doTestInTransaction(new FailureTest() + { + @Override + public void run() + { + dispositionService.getAssociatedDispositionSchedule(folder); + } + }); + } + + /** + * @see DispositionService#getAssociatedDispositionSchedule(NodeRef) + */ + public void testGetAssociatedDispositionScheduleMultiHier() throws Exception + { + doTestInTransaction(new Test() + { + @Override + public Void run() + { + assertNull(dispositionService.getAssociatedDispositionSchedule(mhContainer)); + + // Level 1 + doCheck(mhContainer11, "ds11", false); + doCheck(mhContainer12, "ds12", false); + + // Level 2 + assertNull(dispositionService.getAssociatedDispositionSchedule(mhContainer21)); + assertNull(dispositionService.getAssociatedDispositionSchedule(mhContainer22)); + doCheck(mhContainer23, "ds23", false); + + // Level 3 + assertNull(dispositionService.getAssociatedDispositionSchedule(mhContainer31)); + assertNull(dispositionService.getAssociatedDispositionSchedule(mhContainer32)); + doCheck(mhContainer33, "ds33", true); + assertNull(dispositionService.getAssociatedDispositionSchedule(mhContainer34)); + doCheck(mhContainer35, "ds35", true); + + return null; + } + + private void doCheck(NodeRef container, String dispositionInstructions, boolean isRecordLevel) + { + DispositionSchedule ds = dispositionService.getAssociatedDispositionSchedule(container); + assertNotNull(ds); + checkDispositionSchedule(ds, dispositionInstructions, DEFAULT_DISPOSITION_AUTHORITY, isRecordLevel); + } + }); + } + + /** + * @see DispositionService#hasDisposableItems(DispositionSchedule) + */ + public void testHasDisposableItems() throws Exception + { + doTestInTransaction(new Test() + { + @Override + public Void run() + { + // Add a new disposition schedule + NodeRef container = rmService.createRecordCategory(rmContainer, "hasDisposableTest"); + DispositionSchedule ds = createBasicDispositionSchedule(container); + + assertTrue(dispositionService.hasDisposableItems(dispositionSchedule)); + assertFalse(dispositionService.hasDisposableItems(ds)); + + return null; + } + }); + } + + /** + * @see DispositionService#hasDisposableItems(DispositionSchedule) + */ + public void testHasDisposableItemsMultiHier() throws Exception + { + doTestInTransaction(new Test() + { + @Override + public Void run() + { + assertTrue(dispositionService.hasDisposableItems(mhDispositionSchedule11)); + assertTrue(dispositionService.hasDisposableItems(mhDispositionSchedule12)); + assertTrue(dispositionService.hasDisposableItems(mhDispositionSchedule23)); + assertFalse(dispositionService.hasDisposableItems(mhDispositionSchedule33)); + assertFalse(dispositionService.hasDisposableItems(mhDispositionSchedule35)); + + return null; + } + }); + } + + /** + * @see DispositionService#getDisposableItems(DispositionSchedule) + */ + public void testGetDisposableItems() throws Exception + { + doTestInTransaction(new Test() + { + @Override + public Void run() + { + List nodeRefs = dispositionService.getDisposableItems(dispositionSchedule); + assertNotNull(nodeRefs); + assertEquals(1, nodeRefs.size()); + assertTrue(nodeRefs.contains(rmFolder)); + + return null; + } + }); + } + + /** + * @see DispositionService#getDisposableItems(DispositionSchedule) + */ + public void testGetDisposableItemsMultiHier() throws Exception + { + doTestInTransaction(new Test() + { + @Override + public Void run() + { + List nodeRefs = dispositionService.getDisposableItems(mhDispositionSchedule11); + assertNotNull(nodeRefs); + assertEquals(1, nodeRefs.size()); + assertTrue(nodeRefs.contains(mhRecordFolder41)); + + nodeRefs = dispositionService.getDisposableItems(mhDispositionSchedule12); + assertNotNull(nodeRefs); + assertEquals(1, nodeRefs.size()); + assertTrue(nodeRefs.contains(mhRecordFolder42)); + + nodeRefs = dispositionService.getDisposableItems(mhDispositionSchedule23); + assertNotNull(nodeRefs); + assertEquals(1, nodeRefs.size()); + assertTrue(nodeRefs.contains(mhRecordFolder44)); + + nodeRefs = dispositionService.getDisposableItems(mhDispositionSchedule33); + assertNotNull(nodeRefs); + assertEquals(0, nodeRefs.size()); + + nodeRefs = dispositionService.getDisposableItems(mhDispositionSchedule35); + assertNotNull(nodeRefs); + assertEquals(0, nodeRefs.size()); + + return null; + } + }); + } + + /** + * @see DispositionService#createDispositionSchedule(NodeRef, Map) + */ + public void testCreateDispositionSchedule() throws Exception + { + // Test: simple disposition create + doTestInTransaction(new Test() + { + @Override + public NodeRef run() + { + // Create a new container + NodeRef container = rmService.createRecordCategory(filePlan, "testCreateDispositionSchedule"); + + // Create a new disposition schedule + createBasicDispositionSchedule(container, "testCreateDispositionSchedule", "testCreateDispositionSchedule", false, true); + + return container; + } + + @Override + public void test(NodeRef result) throws Exception + { + // Get the created disposition schedule + DispositionSchedule ds = dispositionService.getAssociatedDispositionSchedule(result); + assertNotNull(ds); + + // Check the disposition schedule + checkDispositionSchedule(ds, "testCreateDispositionSchedule", "testCreateDispositionSchedule", false); + } + }); + + // Failure: create disposition schedule on container with existing disposition schedule + doTestInTransaction(new FailureTest + ( + "Can not create a disposition schedule on a container with an existing disposition schedule" + ) + { + @Override + public void run() + { + createBasicDispositionSchedule(rmContainer); + } + }); + } + + /** + * @see DispositionService#createDispositionSchedule(NodeRef, Map) + */ + public void testCreateDispositionScheduleMultiHier() throws Exception + { + // Test: simple disposition create + doTestInTransaction(new Test() + { + @Override + public Void run() + { + // Create a new structure container + NodeRef testA = rmService.createRecordCategory(mhContainer, "testA"); + NodeRef testB = rmService.createRecordCategory(testA, "testB"); + + // Create new disposition schedules + createBasicDispositionSchedule(testA, "testA", "testA", false, true); + createBasicDispositionSchedule(testB, "testB", "testB", false, true); + + // Add created containers to model + setNodeRef("testA", testA); + setNodeRef("testB", testB); + + return null; + } + + @Override + public void test(Void result) throws Exception + { + // Get the created disposition schedule + DispositionSchedule testA = dispositionService.getAssociatedDispositionSchedule(getNodeRef("testA")); + assertNotNull(testA); + DispositionSchedule testB = dispositionService.getAssociatedDispositionSchedule(getNodeRef("testB")); + assertNotNull(testB); + + // Check the disposition schedule + checkDispositionSchedule(testA, "testA", "testA", false); + checkDispositionSchedule(testB, "testB", "testB", false); + } + }); + + // Failure: create disposition schedule on container with existing disposition schedule + doTestInTransaction(new FailureTest + ( + "Can not create a disposition schedule on container with an existing disposition schedule" + ) + { + @Override + public void run() + { + createBasicDispositionSchedule(mhContainer11); + } + }); + + // Failure: create disposition schedule on a container where there are disposable items under management + doTestInTransaction(new FailureTest + ( + "Can not create a disposition schedule on a container where there are already disposable items under management" + ) + { + @Override + public void run() + { + createBasicDispositionSchedule(mhContainer21); + } + }); + } + + /** + * @see DispositionService#getAssociatedRecordsManagementContainer(DispositionSchedule) + */ + public void testGetAssociatedRecordsManagementContainer() throws Exception + { + doTestInTransaction(new Test() + { + @Override + public Void run() + { + NodeRef nodeRef = dispositionService.getAssociatedRecordsManagementContainer(dispositionSchedule); + assertNotNull(nodeRef); + assertEquals(rmContainer, nodeRef); + + return null; + } + }); + } + + /** + * @see DispositionService#getAssociatedRecordsManagementContainer(DispositionSchedule) + */ + public void testGetAssociatedRecordsManagementContainerMultiHier() throws Exception + { + doTestInTransaction(new Test() + { + @Override + public Void run() + { + NodeRef nodeRef = dispositionService.getAssociatedRecordsManagementContainer(mhDispositionSchedule11); + assertNotNull(nodeRef); + assertEquals(mhContainer11, nodeRef); + + nodeRef = dispositionService.getAssociatedRecordsManagementContainer(mhDispositionSchedule12); + assertNotNull(nodeRef); + assertEquals(mhContainer12, nodeRef); + + nodeRef = dispositionService.getAssociatedRecordsManagementContainer(mhDispositionSchedule23); + assertNotNull(nodeRef); + assertEquals(mhContainer23, nodeRef); + + nodeRef = dispositionService.getAssociatedRecordsManagementContainer(mhDispositionSchedule33); + assertNotNull(nodeRef); + assertEquals(mhContainer33, nodeRef); + + nodeRef = dispositionService.getAssociatedRecordsManagementContainer(mhDispositionSchedule35); + assertNotNull(nodeRef); + assertEquals(mhContainer35, nodeRef); + + return null; + } + }); + } + + // TODO DispositionActionDefinition addDispositionActionDefinition + + // TODO void removeDispositionActionDefinition( + + private NodeRef record43; + private NodeRef record45; + + public void testUpdateDispositionActionDefinitionMultiHier() throws Exception + { + doTestInTransaction(new Test() + { + @Override + public Void run() throws Exception + { + record43 = createRecord(mhRecordFolder43, "record1.txt"); + record45 = createRecord(mhRecordFolder45, "record2.txt"); + + return null; + } + }); + + doTestInTransaction(new Test() + { + @Override + public Void run() throws Exception + { + // Check all the current record folders first + checkDisposableItemUnchanged(mhRecordFolder41); + checkDisposableItemUnchanged(mhRecordFolder42); + checkDisposableItemUnchanged(record43); + checkDisposableItemUnchanged(mhRecordFolder44); + checkDisposableItemUnchanged(record45); + + updateDispositionScheduleOnContainer(mhContainer11); + + return null; + } + + @Override + public void test(Void result) throws Exception + { + // Check all the current record folders first + checkDisposableItemChanged(mhRecordFolder41); + checkDisposableItemUnchanged(mhRecordFolder42); + checkDisposableItemUnchanged(record43); + checkDisposableItemUnchanged(mhRecordFolder44); + checkDisposableItemUnchanged(record45);; + } + }); + + doTestInTransaction(new Test() + { + @Override + public Void run() throws Exception + { + updateDispositionScheduleOnContainer(mhContainer12); + + return null; + } + + @Override + public void test(Void result) throws Exception + { + // Check all the current record folders first + checkDisposableItemChanged(mhRecordFolder41); + checkDisposableItemChanged(mhRecordFolder42); + checkDisposableItemUnchanged(record43); + checkDisposableItemUnchanged(mhRecordFolder44); + checkDisposableItemUnchanged(record45);; + } + }); + + doTestInTransaction(new Test() + { + @Override + public Void run() throws Exception + { + updateDispositionScheduleOnContainer(mhContainer33); + + return null; + } + + @Override + public void test(Void result) throws Exception + { + // Check all the current record folders first + checkDisposableItemChanged(mhRecordFolder41); + checkDisposableItemChanged(mhRecordFolder42); + checkDisposableItemChanged(record43); + checkDisposableItemUnchanged(mhRecordFolder44); + checkDisposableItemUnchanged(record45);; + } + }); + + doTestInTransaction(new Test() + { + @Override + public Void run() throws Exception + { + updateDispositionScheduleOnContainer(mhContainer23); + + return null; + } + + @Override + public void test(Void result) throws Exception + { + // Check all the current record folders first + checkDisposableItemChanged(mhRecordFolder41); + checkDisposableItemChanged(mhRecordFolder42); + checkDisposableItemChanged(record43); + checkDisposableItemChanged(mhRecordFolder44); + checkDisposableItemUnchanged(record45); + } + }); + + doTestInTransaction(new Test() + { + @Override + public Void run() throws Exception + { + updateDispositionScheduleOnContainer(mhContainer35); + + return null; + } + + @Override + public void test(Void result) throws Exception + { + // Check all the current record folders first + checkDisposableItemChanged(mhRecordFolder41); + checkDisposableItemChanged(mhRecordFolder42); + checkDisposableItemChanged(record43); + checkDisposableItemChanged(mhRecordFolder44); + checkDisposableItemChanged(record45); + } + }); + } + + private void publishDispositionActionDefinitionChange(DispositionActionDefinition dad) + { + PublishExecutorRegistry reg = (PublishExecutorRegistry)applicationContext.getBean("publishExecutorRegistry"); + PublishExecutor pub = reg.get(RecordsManagementModel.UPDATE_TO_DISPOSITION_ACTION_DEFINITION); + assertNotNull(pub); + pub.publish(dad.getNodeRef()); + } + + private void checkDisposableItemUnchanged(NodeRef recordFolder) + { + checkDispositionAction( + dispositionService.getNextDispositionAction(recordFolder), + "cutoff", + new String[]{DEFAULT_EVENT_NAME}, + PERIOD_NONE); + } + + private void checkDisposableItemChanged(NodeRef recordFolder) throws Exception + { + checkDispositionAction( + dispositionService.getNextDispositionAction(recordFolder), + "cutoff", + new String[]{DEFAULT_EVENT_NAME, "abolished"}, + "week|1"); + } + + private void updateDispositionScheduleOnContainer(NodeRef nodeRef) + { + Map updateProps = new HashMap(3); + updateProps.put(PROP_DISPOSITION_PERIOD, "week|1"); + updateProps.put(PROP_DISPOSITION_EVENT, (Serializable)Arrays.asList(DEFAULT_EVENT_NAME, "abolished")); + + DispositionSchedule ds = dispositionService.getDispositionSchedule(nodeRef); + DispositionActionDefinition dad = ds.getDispositionActionDefinitionByName("cutoff"); + dispositionService.updateDispositionActionDefinition(dad, updateProps); + publishDispositionActionDefinitionChange(dad); + } + + /** + * + * @param da + * @param name + * @param arrEventNames + * @param strPeriod + */ + private void checkDispositionAction(DispositionAction da, String name, String[] arrEventNames, String strPeriod) + { + assertNotNull(da); + assertEquals(name, da.getName()); + + List events = da.getEventCompletionDetails(); + assertNotNull(events); + assertEquals(arrEventNames.length, events.size()); + + List origEvents = new ArrayList(events.size()); + for (EventCompletionDetails event : events) + { + origEvents.add(event.getEventName()); + } + + List expectedEvents = Arrays.asList(arrEventNames); + Collection copy = new ArrayList(origEvents); + + for (Iterator i = origEvents.iterator(); i.hasNext(); ) + { + String origEvent = i.next(); + + if (expectedEvents.contains(origEvent) == true) + { + i.remove(); + copy.remove(origEvent); + } + } + + if (copy.size() != 0 && expectedEvents.size() != 0) + { + StringBuffer buff = new StringBuffer(255); + if (copy.size() != 0) + { + buff.append("The following events where found, but not expected: ("); + for (String eventName : copy) + { + buff.append(eventName).append(", "); + } + buff.append("). "); + } + if (expectedEvents.size() != 0) + { + buff.append("The following events where not found, but expected: ("); + for (String eventName : expectedEvents) + { + buff.append(eventName).append(", "); + } + buff.append(")."); + } + fail(buff.toString()); + } + + if (PERIOD_NONE.equals(strPeriod) == true) + { + assertNull(da.getAsOfDate()); + } + else + { + assertNotNull(da.getAsOfDate()); + } + } + + // TODO boolean isNextDispositionActionEligible(NodeRef nodeRef); + + // TODO DispositionAction getNextDispositionAction(NodeRef nodeRef); + + // TODO List getCompletedDispositionActions(NodeRef nodeRef); + + // TODO DispositionAction getLastCompletedDispostionAction(NodeRef nodeRef); + + // TODO List getDispositionPeriodProperties(); + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RMCaveatConfigServiceImplTest.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RMCaveatConfigServiceImplTest.java new file mode 100644 index 0000000000..e36de6ef56 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RMCaveatConfigServiceImplTest.java @@ -0,0 +1,612 @@ +/* + * 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.module.org_alfresco_module_rm.test.service; + +import java.util.ArrayList; +import java.util.List; + +import javax.transaction.UserTransaction; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.caveat.RMCaveatConfigService; +import org.alfresco.module.org_alfresco_module_rm.caveat.RMCaveatConfigServiceImpl; +import org.alfresco.module.org_alfresco_module_rm.dod5015.DOD5015Model; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +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.security.AuthorityService; +import org.alfresco.service.cmr.security.AuthorityType; +import org.alfresco.service.cmr.security.MutableAuthenticationService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.BaseSpringTest; +import org.alfresco.util.PropertyMap; + +/** + * Test of RM Caveat (Admin facing scripts) + * + * @author Mark Rogers + */ +public class RMCaveatConfigServiceImplTest extends BaseSpringTest implements DOD5015Model +{ + protected static StoreRef SPACES_STORE = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "SpacesStore"); + + private NodeRef filePlan; + + private NodeService nodeService; + private TransactionService transactionService; + private RMCaveatConfigService caveatConfigService; + + private MutableAuthenticationService authenticationService; + private PersonService personService; + private AuthorityService authorityService; + + + // example base test data for supplemental markings list + protected final static String NOFORN = "NOFORN"; // Not Releasable to Foreign Nationals/Governments/Non-US Citizens + protected final static String NOCONTRACT = "NOCONTRACT"; // Not Releasable to Contractors or Contractor/Consultants + protected final static String FOUO = "FOUO"; // For Official Use Only + protected final static String FGI = "FGI"; // Foreign Government Information + + protected final static String RM_LIST = "rmc:smList"; // existing pre-defined list + protected final static String RM_LIST_ALT = "rmc:anoList"; + + @Override + protected void onSetUpInTransaction() throws Exception + { + super.onSetUpInTransaction(); + + // Get the service required in the tests + this.nodeService = (NodeService)this.applicationContext.getBean("NodeService"); // use upper 'N'odeService (to test access config interceptor) + this.authenticationService = (MutableAuthenticationService)this.applicationContext.getBean("AuthenticationService"); + this.personService = (PersonService)this.applicationContext.getBean("PersonService"); + this.authorityService = (AuthorityService)this.applicationContext.getBean("AuthorityService"); + this.caveatConfigService = (RMCaveatConfigServiceImpl)this.applicationContext.getBean("caveatConfigService"); + this.transactionService = (TransactionService)this.applicationContext.getBean("TransactionService"); + + + // Set the current security context as admin + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + + // Get the test data + setUpTestData(); + } + + private void setUpTestData() + { + } + + @Override + protected void onTearDownInTransaction() throws Exception + { + try + { + UserTransaction txn = transactionService.getUserTransaction(false); + txn.begin(); + this.nodeService.deleteNode(filePlan); + txn.commit(); + } + catch (Exception e) + { + // Nothing + //System.out.println("DID NOT DELETE FILE PLAN!"); + } + } + + @Override + protected void onTearDownAfterTransaction() throws Exception + { + // TODO Auto-generated method stub + super.onTearDownAfterTransaction(); + } + + public void testSetup() + { + // NOOP + } + + + /** + * Test of Caveat Config + * + * @throws Exception + */ + public void testAddRMConstraintList() throws Exception + { + setComplete(); + endTransaction(); + + cleanCaveatConfigData(); + + startNewTransaction(); + + /** + * Now remove the entire list (rma:smList); + */ + logger.debug("test remove entire list rmc:smList"); + caveatConfigService.deleteRMConstraint(RM_LIST); + + /** + * Now add the list again + */ + logger.debug("test add back rmc:smList"); + caveatConfigService.addRMConstraint(RM_LIST, "my title", new String[0]); + + /** + * Negative test - add a list that already exists + */ + logger.debug("try to create duplicate list rmc:smList"); + caveatConfigService.addRMConstraint(RM_LIST, "my title", new String[0]); + + /** + * Negative test - remove a list that does not exist + */ + logger.debug("test remove entire list rmc:smList"); + caveatConfigService.deleteRMConstraint(RM_LIST); + try + { + caveatConfigService.deleteRMConstraint(RM_LIST); + fail("unknown constraint should have thrown an exception"); + } + catch (Exception e) + { + // expect to go here + } + + + /** + * Negative test - add a constraint to property that does not exist + */ + logger.debug("test property does not exist"); + try + { + caveatConfigService.addRMConstraint("rma:mer", "", new String[0]); + fail("unknown property should have thrown an exception"); + } + catch (Exception e) + { + // expect to go here + } + endTransaction(); + cleanCaveatConfigData(); + + } + + /** + * Test of addRMConstraintListValue + * + * @throws Exception + */ + public void testAddRMConstraintListValue() throws Exception + { + setComplete(); + endTransaction(); + + cleanCaveatConfigData(); + setupCaveatConfigData(); + + startNewTransaction(); + caveatConfigService.addRMConstraint(RM_LIST, "my title", new String[0]); + + /** + * Add a user to the list + */ + List values = new ArrayList(); + values.add(NOFORN); + values.add(NOCONTRACT); + caveatConfigService.updateRMConstraintListAuthority(RM_LIST, "jrogers", values); + + /** + * Add another value to that list + */ + caveatConfigService.addRMConstraintListValue(RM_LIST, "jrogers", FGI); + + /** + * Negative test - attempt to add a duplicate value + */ + caveatConfigService.addRMConstraintListValue(RM_LIST, "jrogers", FGI); + + /** + * Negative test - attempt to add to a list that does not exist + */ + try + { + caveatConfigService.addRMConstraintListValue(RM_LIST_ALT, "mhouse", FGI); + fail("exception not thrown"); + } + catch (Exception re) + { + // should go here + + } + + /** + * Negative test - attempt to add to a list that does exist and user that does not exist + */ + try + { + caveatConfigService.addRMConstraintListValue(RM_LIST, "mhouse", FGI); + fail("exception not thrown"); + } + catch (Exception e) + { + // should go here + } + + } + + + /** + * Test of UpdateRMConstraintListAuthority + * + * @throws Exception + */ + public void testUpdateRMConstraintListAuthority() throws Exception + { + setComplete(); + endTransaction(); + + cleanCaveatConfigData(); + setupCaveatConfigData(); + + startNewTransaction(); + + caveatConfigService.addRMConstraint(RM_LIST, "my title", new String[0]); + + /** + * Add a user to the list + */ + List values = new ArrayList(); + values.add(NOFORN); + values.add(NOCONTRACT); + caveatConfigService.updateRMConstraintListAuthority(RM_LIST, "jrogers", values); + + /** + * Add to a authority that already exists + * Should replace existing authority + */ + List updatedValues = new ArrayList(); + values.add(FGI); + caveatConfigService.updateRMConstraintListAuthority(RM_LIST, "jrogers", updatedValues); + + /** + * Add a group to the list + */ + caveatConfigService.updateRMConstraintListAuthority(RM_LIST, "Engineering", values); + + /** + * Add to a list that does not exist + * Should create a new list + */ + caveatConfigService.deleteRMConstraint(RM_LIST); + caveatConfigService.updateRMConstraintListAuthority(RM_LIST, "jrogers", values); + + + /** + * Add to a authority that already exists + * Should replace existing authority + */ + + endTransaction(); + cleanCaveatConfigData(); + + } + + /** + * Test of RemoveRMConstraintListAuthority + * + * @throws Exception + */ + public void testRemoveRMConstraintListAuthority() throws Exception + { + setComplete(); + endTransaction(); + + cleanCaveatConfigData(); + setupCaveatConfigData(); + + startNewTransaction(); + caveatConfigService.addRMConstraint(RM_LIST, "my title", new String[0]); + + List values = new ArrayList(); + values.add(FGI); + caveatConfigService.updateRMConstraintListAuthority(RM_LIST, "jrogers", values); + + /** + * Remove a user from a list + */ + caveatConfigService.removeRMConstraintListAuthority(RM_LIST, "jrogers"); + + /** + * Negative test - remove a user that does not exist + */ + caveatConfigService.removeRMConstraintListAuthority(RM_LIST, "jrogers"); + + /** + * Negative test - remove a user from a list that does not exist. + * Should create a new list + */ + + caveatConfigService.addRMConstraint(RM_LIST, "my title", new String[0]); + caveatConfigService.updateRMConstraintListAuthority(RM_LIST, "jrogers", values); + + endTransaction(); + cleanCaveatConfigData(); + + } + + + + + /** + * Test of Caveat Config + * + * @throws Exception + */ + public void testRMCaveatConfig() throws Exception + { + setComplete(); + endTransaction(); + + cleanCaveatConfigData(); + + startNewTransaction(); + + caveatConfigService.addRMConstraint(RM_LIST, "my title", new String[0]); + + List values = new ArrayList(); + values.add(NOFORN); + values.add(FOUO); + caveatConfigService.updateRMConstraintListAuthority(RM_LIST, "dfranco", values); + + values.add(FGI); + values.add(NOCONTRACT); + caveatConfigService.updateRMConstraintListAuthority(RM_LIST, "dmartinz", values); + + // Test list of allowed values for caveats + + List allowedValues = AuthenticationUtil.runAs(new RunAsWork>() + { + public List doWork() + { + // get allowed values for given caveat (for current user) + return caveatConfigService.getRMAllowedValues(RM_LIST); + } + }, "dfranco"); + + assertEquals(2, allowedValues.size()); + assertTrue(allowedValues.contains(NOFORN)); + assertTrue(allowedValues.contains(FOUO)); + + + allowedValues = AuthenticationUtil.runAs(new RunAsWork>() + { + public List doWork() + { + // get allowed values for given caveat (for current user) + return caveatConfigService.getRMAllowedValues(RM_LIST); + } + }, "dmartinz"); + + assertEquals(4, allowedValues.size()); + assertTrue(allowedValues.contains(NOFORN)); + assertTrue(allowedValues.contains(NOCONTRACT)); + assertTrue(allowedValues.contains(FOUO)); + assertTrue(allowedValues.contains(FGI)); + + /** + // + * Now remove the entire list (rma:smList); + */ + logger.debug("test remove entire list rmc:smList"); + caveatConfigService.deleteRMConstraint(RM_LIST); + + + /** + * Now add the list again + */ + logger.debug("test add back rmc:smList"); + caveatConfigService.addRMConstraint(RM_LIST, "my title", new String[0]); + + /** + * Negative test - add a list that already exists + */ + logger.debug("try to create duplicate list rmc:smList"); + caveatConfigService.addRMConstraint(RM_LIST, "my title", new String[0]); + + /** + * Negative test - remove a list that does not exist + */ + logger.debug("test remove entire list rmc:smList"); + caveatConfigService.deleteRMConstraint(RM_LIST); + try + { + caveatConfigService.deleteRMConstraint(RM_LIST); + fail("unknown constraint should have thrown an exception"); + } + catch (Exception e) + { + // expect to go here + } + + + /** + * Negative test - add a constraint to property that does not exist + */ + logger.debug("test property does not exist"); + try + { + caveatConfigService.addRMConstraint("rma:mer", "", new String[0]); + fail("unknown property should have thrown an exception"); + } + catch (Exception e) + { + // expect to go here + } + endTransaction(); + cleanCaveatConfigData(); + } + + private void cleanCaveatConfigData() + { + startNewTransaction(); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + + deleteUser("jrangel"); + deleteUser("dmartinz"); + deleteUser("jrogers"); + deleteUser("hmcneil"); + deleteUser("dfranco"); + deleteUser("gsmith"); + deleteUser("eharris"); + deleteUser("bbayless"); + deleteUser("mhouse"); + deleteUser("aly"); + deleteUser("dsandy"); + deleteUser("driggs"); + deleteUser("test1"); + + deleteGroup("Engineering"); + deleteGroup("Finance"); + deleteGroup("test1"); + + caveatConfigService.updateOrCreateCaveatConfig("{}"); // empty config ! + + setComplete(); + endTransaction(); + } + + private void setupCaveatConfigData() + { + startNewTransaction(); + + // Switch to admin + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + + // Create test users/groups (if they do not already exist) + + createUser("jrangel"); + createUser("dmartinz"); + createUser("jrogers"); + createUser("hmcneil"); + createUser("dfranco"); + createUser("gsmith"); + createUser("eharris"); + createUser("bbayless"); + createUser("mhouse"); + createUser("aly"); + createUser("dsandy"); + createUser("driggs"); + createUser("test1"); + + createGroup("Engineering"); + createGroup("Finance"); + createGroup("test1"); + + addToGroup("jrogers", "Engineering"); + addToGroup("dfranco", "Finance"); + + // not in grouo to start with - added later + //addToGroup("gsmith", "Engineering"); + + + //URL url = AbstractContentTransformerTest.class.getClassLoader().getResource("testCaveatConfig2.json"); // from test-resources + //assertNotNull(url); + //File file = new File(url.getFile()); + //assertTrue(file.exists()); + + //caveatConfigService.updateOrCreateCaveatConfig(file); + + setComplete(); + endTransaction(); + } + + protected void createUser(String userName) + { + if (! authenticationService.authenticationExists(userName)) + { + authenticationService.createAuthentication(userName, "PWD".toCharArray()); + } + + if (! personService.personExists(userName)) + { + PropertyMap ppOne = new PropertyMap(4); + ppOne.put(ContentModel.PROP_USERNAME, userName); + ppOne.put(ContentModel.PROP_FIRSTNAME, "firstName"); + ppOne.put(ContentModel.PROP_LASTNAME, "lastName"); + ppOne.put(ContentModel.PROP_EMAIL, "email@email.com"); + ppOne.put(ContentModel.PROP_JOBTITLE, "jobTitle"); + + personService.createPerson(ppOne); + } + } + + protected void deleteUser(String userName) + { + if (personService.personExists(userName)) + { + personService.deletePerson(userName); + } + } + + protected void createGroup(String groupShortName) + { + createGroup(null, groupShortName); + } + + protected void createGroup(String parentGroupShortName, String groupShortName) + { + if (parentGroupShortName != null) + { + String parentGroupFullName = authorityService.getName(AuthorityType.GROUP, parentGroupShortName); + if (authorityService.authorityExists(parentGroupFullName) == false) + { + authorityService.createAuthority(AuthorityType.GROUP, groupShortName, groupShortName, null); + authorityService.addAuthority(parentGroupFullName, groupShortName); + } + } + else + { + authorityService.createAuthority(AuthorityType.GROUP, groupShortName, groupShortName, null); + } + } + + protected void deleteGroup(String groupShortName) + { + String groupFullName = authorityService.getName(AuthorityType.GROUP, groupShortName); + if (authorityService.authorityExists(groupFullName) == true) + { + authorityService.deleteAuthority(groupFullName); + } + } + + protected void addToGroup(String authorityName, String groupShortName) + { + authorityService.addAuthority(authorityService.getName(AuthorityType.GROUP, groupShortName), authorityName); + } + + protected void removeFromGroup(String authorityName, String groupShortName) + { + authorityService.removeAuthority(authorityService.getName(AuthorityType.GROUP, groupShortName), authorityName); + } + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementActionServiceImplTest.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementActionServiceImplTest.java new file mode 100644 index 0000000000..aaef219d15 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementActionServiceImplTest.java @@ -0,0 +1,290 @@ +/* + * 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.module.org_alfresco_module_rm.test.service; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import junit.framework.TestCase; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementPolicies; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementPolicies.BeforeRMActionExecution; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementPolicies.OnRMActionExecution; +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction; +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.module.org_alfresco_module_rm.test.util.TestAction; +import org.alfresco.module.org_alfresco_module_rm.test.util.TestAction2; +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.policy.Behaviour.NotificationFrequency; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.ServiceRegistry; +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.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ApplicationContextHelper; +import org.springframework.context.ApplicationContext; + +/** + * Records management action service implementation test + * + * @author Roy Wetherall + */ +public class RecordsManagementActionServiceImplTest extends TestCase + implements RecordsManagementModel, + BeforeRMActionExecution, + OnRMActionExecution +{ + private static final String[] CONFIG_LOCATIONS = new String[] { + "classpath:alfresco/application-context.xml", + "classpath:org/alfresco/module/org_alfresco_module_rm/test/util/test-context.xml"}; + + private ApplicationContext ctx; + + private ServiceRegistry serviceRegistry; + private TransactionService transactionService; + private RetryingTransactionHelper txnHelper; + private NodeService nodeService; + private RecordsManagementActionService rmActionService; + private PolicyComponent policyComponent; + + private NodeRef nodeRef; + private List nodeRefs; + + private boolean beforeMarker; + private boolean onMarker; + private boolean inTest; + + @Override + protected void setUp() throws Exception + { + ctx = ApplicationContextHelper.getApplicationContext(CONFIG_LOCATIONS); + + this.serviceRegistry = (ServiceRegistry) ctx.getBean(ServiceRegistry.SERVICE_REGISTRY); + this.transactionService = serviceRegistry.getTransactionService(); + this.txnHelper = transactionService.getRetryingTransactionHelper(); + this.nodeService = serviceRegistry.getNodeService(); + + this.rmActionService = (RecordsManagementActionService)ctx.getBean("RecordsManagementActionService"); + this.policyComponent = (PolicyComponent)ctx.getBean("policyComponent"); + + // Set the current security context as admin + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + + RetryingTransactionCallback setUpCallback = new RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + // Create a node we can use for the tests + NodeRef rootNodeRef = nodeService.getRootNode(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE); + nodeRef = nodeService.createNode( + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(NamespaceService.CONTENT_MODEL_PREFIX, "temp.txt"), + ContentModel.TYPE_CONTENT).getChildRef(); + + // Create nodeRef list + nodeRefs = new ArrayList(5); + for (int i = 0; i < 5; i++) + { + nodeRefs.add( + nodeService.createNode( + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(NamespaceService.CONTENT_MODEL_PREFIX, "temp.txt"), + ContentModel.TYPE_CONTENT).getChildRef()); + } + return null; + } + }; + txnHelper.doInTransaction(setUpCallback); + + beforeMarker = false; + onMarker = false; + inTest = false; + } + + @Override + protected void tearDown() + { + AuthenticationUtil.clearCurrentSecurityContext(); + } + + public void testGetActions() + { + RetryingTransactionCallback testCallback = new RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + getActionsImpl(); + return null; + } + }; + txnHelper.doInTransaction(testCallback); + } + + private void getActionsImpl() + { + List result = this.rmActionService.getRecordsManagementActions(); + assertNotNull(result); + Map resultMap = new HashMap(8); + for (RecordsManagementAction action : result) + { + resultMap.put(action.getName(), action); + } + + assertTrue(resultMap.containsKey(TestAction.NAME)); + assertTrue(resultMap.containsKey(TestAction2.NAME)); + + result = this.rmActionService.getDispositionActions(); + resultMap = new HashMap(8); + for (RecordsManagementAction action : result) + { + resultMap.put(action.getName(), action); + } + assertTrue(resultMap.containsKey(TestAction.NAME)); + assertFalse(resultMap.containsKey(TestAction2.NAME)); + + // get some specific actions and check the label + RecordsManagementAction cutoff = this.rmActionService.getDispositionAction("cutoff"); + assertNotNull(cutoff); + assertEquals("Cutoff", cutoff.getLabel()); + assertEquals("Cutoff", cutoff.getDescription()); + + RecordsManagementAction freeze = this.rmActionService.getRecordsManagementAction("freeze"); + assertNotNull(freeze); + assertEquals("Freeze", freeze.getLabel()); + assertEquals("Freeze", freeze.getLabel()); + + // test non-existent actions + assertNull(this.rmActionService.getDispositionAction("notThere")); + assertNull(this.rmActionService.getRecordsManagementAction("notThere")); + } + + public void testExecution() + { + RetryingTransactionCallback testCallback = new RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + executionImpl(); + return null; + } + }; + txnHelper.doInTransaction(testCallback); + } + + public void beforeRMActionExecution(NodeRef nodeRef, String name, Map parameters) + { + if (inTest == true) + { + assertEquals(this.nodeRef, nodeRef); + assertEquals(TestAction.NAME, name); + assertEquals(1, parameters.size()); + assertTrue(parameters.containsKey(TestAction.PARAM)); + assertEquals(TestAction.PARAM_VALUE, parameters.get(TestAction.PARAM)); + beforeMarker = true; + } + } + + public void onRMActionExecution(NodeRef nodeRef, String name, Map parameters) + { + if (inTest == true) + { + assertEquals(this.nodeRef, nodeRef); + assertEquals(TestAction.NAME, name); + assertEquals(1, parameters.size()); + assertTrue(parameters.containsKey(TestAction.PARAM)); + assertEquals(TestAction.PARAM_VALUE, parameters.get(TestAction.PARAM)); + onMarker = true; + } + } + + private void executionImpl() + { + inTest = true; + try + { + policyComponent.bindClassBehaviour( + RecordsManagementPolicies.BEFORE_RM_ACTION_EXECUTION, + this, + new JavaBehaviour(this, "beforeRMActionExecution", NotificationFrequency.EVERY_EVENT)); + policyComponent.bindClassBehaviour( + RecordsManagementPolicies.ON_RM_ACTION_EXECUTION, + this, + new JavaBehaviour(this, "onRMActionExecution", NotificationFrequency.EVERY_EVENT)); + + assertFalse(beforeMarker); + assertFalse(onMarker); + assertFalse(this.nodeService.hasAspect(this.nodeRef, ASPECT_RECORD)); + + Map params = new HashMap(1); + params.put(TestAction.PARAM, TestAction.PARAM_VALUE); + this.rmActionService.executeRecordsManagementAction(this.nodeRef, TestAction.NAME, params); + + assertTrue(beforeMarker); + assertTrue(onMarker); + assertTrue(this.nodeService.hasAspect(this.nodeRef, ASPECT_RECORD)); + } + finally + { + inTest = false; + } + } + + public void testBulkExecution() + { + RetryingTransactionCallback testCallback = new RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + bulkExecutionImpl(); + return null; + } + }; + txnHelper.doInTransaction(testCallback); + } + + private void bulkExecutionImpl() + { + for (NodeRef nodeRef : this.nodeRefs) + { + assertFalse(this.nodeService.hasAspect(nodeRef, ASPECT_RECORD)); + } + + Map params = new HashMap(1); + params.put(TestAction.PARAM, TestAction.PARAM_VALUE); + this.rmActionService.executeRecordsManagementAction(this.nodeRefs, TestAction.NAME, params); + + for (NodeRef nodeRef : this.nodeRefs) + { + assertTrue(this.nodeService.hasAspect(nodeRef, ASPECT_RECORD)); + } + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementAdminServiceImplTest.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementAdminServiceImplTest.java new file mode 100644 index 0000000000..9574d9d72f --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementAdminServiceImplTest.java @@ -0,0 +1,952 @@ +/* + * 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.module.org_alfresco_module_rm.test.service; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminService; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementPolicies; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementPolicies.BeforeCreateReference; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementPolicies.OnCreateReference; +import org.alfresco.module.org_alfresco_module_rm.caveat.RMListOfValuesConstraint; +import org.alfresco.module.org_alfresco_module_rm.caveat.RMListOfValuesConstraint.MatchLogic; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementCustomModel; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.module.org_alfresco_module_rm.script.CustomReferenceType; +import org.alfresco.module.org_alfresco_module_rm.test.util.BaseRMTestCase; +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.repo.policy.Behaviour.NotificationFrequency; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.cmr.dictionary.AspectDefinition; +import org.alfresco.service.cmr.dictionary.AssociationDefinition; +import org.alfresco.service.cmr.dictionary.Constraint; +import org.alfresco.service.cmr.dictionary.ConstraintDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.alfresco.util.Pair; +import org.springframework.util.CollectionUtils; + +/** + * This test class tests the definition and use of a custom RM elements at the Java services layer. + * + * @author Neil McErlean, janv, Roy Wetherall + */ +public class RecordsManagementAdminServiceImplTest extends BaseRMTestCase + implements RecordsManagementModel, + BeforeCreateReference, + OnCreateReference +{ + + private final static long testRunID = System.currentTimeMillis(); + + private List createdCustomProperties; + private List madeCustomisable; + + /** + * @see org.alfresco.module.org_alfresco_module_rm.test.util.BaseRMTestCase#setUp() + */ + @Override + protected void setUp() throws Exception + { + createdCustomProperties = new ArrayList(); + madeCustomisable = new ArrayList(); + super.setUp(); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.test.util.BaseRMTestCase#setupTestData() + */ + @Override + protected void setupTestData() + { + super.setupTestData(); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.test.util.BaseRMTestCase#tearDown() + */ + @Override + protected void tearDown() throws Exception + { + retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + // As system user + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + + for (QName createdCustomProperty : createdCustomProperties) + { + adminService.removeCustomPropertyDefinition(createdCustomProperty); + } + + for (QName customisable : madeCustomisable) + { + adminService.unmakeCustomisable(customisable); + } + + return null; + } + }); + + super.tearDown(); + } + + /** + * @see RecordsManagementAdminService#getCustomisable() + */ + public void testGetCustomisable() throws Exception + { + // Get the customisable types + doTestInTransaction(new Test() + { + @Override + public Void run() throws Exception + { + Set list = adminService.getCustomisable(); + assertNotNull(list); + assertTrue(list.containsAll( + CollectionUtils.arrayToList(new QName[] + { + ASPECT_RECORD, + TYPE_RECORD_FOLDER, + TYPE_NON_ELECTRONIC_DOCUMENT, + TYPE_RECORD_CATEGORY + }))); + + return null; + } + }); + } + + /** + * @see RecordsManagementAdminService#isCustomisable(QName) + */ + public void testIsCustomisable() throws Exception + { + doTestInTransaction(new Test() + { + @Override + public Void run() throws Exception + { + assertFalse(adminService.isCustomisable(TYPE_CONTENT)); + assertFalse(adminService.isCustomisable(ASPECT_DUBLINCORE)); + assertTrue(adminService.isCustomisable(TYPE_RECORD_FOLDER)); + assertTrue(adminService.isCustomisable(ASPECT_RECORD)); + + return null; + } + }); + } + + /** + * @see RecordsManagementAdminService#existsCustomProperty(QName) + * @see RecordsManagementAdminService#addCustomPropertyDefinition(QName, QName, String, QName, String, String, String, boolean, boolean, boolean, QName) + * @see RecordsManagementAdminService#addCustomPropertyDefinition(QName, QName, String, QName, String, String) + */ + public void testAddCustomPropertyDefinition() throws Exception + { + // Add property to Record (id specified, short version) + doTestInTransaction(new Test() + { + @Override + public QName run() throws Exception + { + // Check the property does not exist + assertFalse(adminService.existsCustomProperty(QName.createQName(RecordsManagementCustomModel.RM_CUSTOM_URI, "myRecordProp1"))); + + return adminService.addCustomPropertyDefinition( + QName.createQName(RecordsManagementCustomModel.RM_CUSTOM_URI, "myRecordProp1"), + ASPECT_RECORD, + "Label1", + DataTypeDefinition.TEXT, + "Title", + "Description"); + } + + @Override + public void test(QName result) throws Exception + { + try + { + // Check the property QName is correct + assertNotNull(result); + assertEquals(QName.createQName(RecordsManagementCustomModel.RM_CUSTOM_URI, "myRecordProp1"), result); + assertTrue(adminService.existsCustomProperty(result)); + + // Check that property is available as a custom property + Map propDefs = adminService.getCustomPropertyDefinitions(ASPECT_RECORD); + assertNotNull(propDefs); + assertTrue(propDefs.containsKey(result)); + + // Check the property definition + PropertyDefinition propDef = propDefs.get(result); + assertNotNull(propDef); + assertEquals(DataTypeDefinition.TEXT, propDef.getDataType().getName()); + assertEquals("Description", propDef.getDescription()); + assertEquals("Label1", propDef.getTitle()); + } + finally + { + // Store the created property for cleanup later + createdCustomProperties.add(result); + } + } + }); + + // Add property to record (no id, short version) + doTestInTransaction(new Test() + { + @Override + public QName run() throws Exception + { + return adminService.addCustomPropertyDefinition( + null, + ASPECT_RECORD, + "Label2", + DataTypeDefinition.TEXT, + "Title", + "Description"); + } + + @Override + public void test(QName result) throws Exception + { + try + { + // Check the property QName is correct + assertNotNull(result); + assertEquals(RecordsManagementCustomModel.RM_CUSTOM_URI, result.getNamespaceURI()); + assertTrue(adminService.existsCustomProperty(result)); + + // Check that property is available as a custom property + Map propDefs = adminService.getCustomPropertyDefinitions(ASPECT_RECORD); + assertNotNull(propDefs); + assertTrue(propDefs.containsKey(result)); + + // Check the property definition + PropertyDefinition propDef = propDefs.get(result); + assertNotNull(propDef); + assertEquals(DataTypeDefinition.TEXT, propDef.getDataType().getName()); + assertEquals("Description", propDef.getDescription()); + assertEquals("Label2", propDef.getTitle()); + } + finally + { + // Store the created property for cleanup later + createdCustomProperties.add(result); + } + } + }); + + // Add property to record (long version) + doTestInTransaction(new Test() + { + @Override + public QName run() throws Exception + { + return adminService.addCustomPropertyDefinition( + null, + ASPECT_RECORD, + "Label3", + DataTypeDefinition.TEXT, + "Title", + "Description", + "default", + false, + false, + false, + null); + } + + @Override + public void test(QName result) throws Exception + { + try + { + // Check the property QName is correct + assertNotNull(result); + assertEquals(RecordsManagementCustomModel.RM_CUSTOM_URI, result.getNamespaceURI()); + assertTrue(adminService.existsCustomProperty(result)); + + // Check that property is available as a custom property + Map propDefs = adminService.getCustomPropertyDefinitions(ASPECT_RECORD); + assertNotNull(propDefs); + //assertEquals(3, propDefs.size()); + assertTrue(propDefs.containsKey(result)); + + // Check the property definition + PropertyDefinition propDef = propDefs.get(result); + assertNotNull(propDef); + assertEquals(DataTypeDefinition.TEXT, propDef.getDataType().getName()); + assertEquals("Description", propDef.getDescription()); + assertEquals("Label3", propDef.getTitle()); + assertEquals("default", propDef.getDefaultValue()); + assertFalse(propDef.isMandatory()); + assertFalse(propDef.isMultiValued()); + assertFalse(propDef.isProtected()); + + } + finally + { + // Store the created property for cleanup later + createdCustomProperties.add(result); + } + } + }); + + // Failure: Add a property with the same name twice + doTestInTransaction(new FailureTest + ( + "Can not create a property with the same id twice" + ) + { + @Override + public void run() + { + adminService.addCustomPropertyDefinition( + QName.createQName(RecordsManagementCustomModel.RM_CUSTOM_URI, "myRecordProp1"), + ASPECT_RECORD, + "Label1", + DataTypeDefinition.TEXT, + "Title", + "Description"); + } + }); + + // Failure: Try and add a property to a type that isn't customisable + doTestInTransaction(new FailureTest + ( + "Can not add a custom property to a type that isn't registered as customisable" + ) + { + @Override + public void run() + { + adminService.addCustomPropertyDefinition( + QName.createQName(RecordsManagementCustomModel.RM_CUSTOM_URI, "myContentProp"), + TYPE_CONTENT, + "Label1", + DataTypeDefinition.TEXT, + "Title", + "Description"); + } + }); + + // Failure: Add a property with the label twice (but no id specified) +// doTestInTransaction(new FailureTest +// ( +// "Can not create a property with the same label twice if no id is specified." +// ) +// { +// @Override +// public void run() +// { +// adminService.addCustomPropertyDefinition( +// null, +// ASPECT_RECORD, +// "Label1", +// DataTypeDefinition.TEXT, +// "Title", +// "Description"); +// } +// }); + } + + /** + * @see RecordsManagementAdminService#makeCustomisable(QName) + */ + public void testMakeCustomisable() throws Exception + { + doTestInTransaction(new Test() + { + @Override + public QName run() throws Exception + { + // Make a type customisable + assertFalse(adminService.isCustomisable(TYPE_CUSTOM_TYPE)); + adminService.makeCustomisable(TYPE_CUSTOM_TYPE); + madeCustomisable.add(TYPE_CUSTOM_TYPE); + assertTrue(adminService.isCustomisable(TYPE_CUSTOM_TYPE)); + + // Add a custom property + return adminService.addCustomPropertyDefinition( + QName.createQName(RecordsManagementCustomModel.RM_CUSTOM_URI, "myNewProperty"), + TYPE_CUSTOM_TYPE, + "Label", + DataTypeDefinition.TEXT, + "Title", + "Description"); + } + + @Override + public void test(QName result) throws Exception + { + // Check the property QName is correct + assertNotNull(result); + assertEquals(QName.createQName(RecordsManagementCustomModel.RM_CUSTOM_URI, "myNewProperty"), result); + assertTrue(adminService.existsCustomProperty(result)); + + // Check that property is available as a custom property + Map propDefs = adminService.getCustomPropertyDefinitions(TYPE_CUSTOM_TYPE); + assertNotNull(propDefs); + assertEquals(1, propDefs.size()); + assertTrue(propDefs.containsKey(result)); + + // Check the property definition + PropertyDefinition propDef = propDefs.get(result); + assertNotNull(propDef); + assertEquals(DataTypeDefinition.TEXT, propDef.getDataType().getName()); + assertEquals("Description", propDef.getDescription()); + assertEquals("Label", propDef.getTitle()); + + } + }); + + doTestInTransaction(new Test() + { + @Override + public QName run() throws Exception + { + // Make an aspect customisable + assertFalse(adminService.isCustomisable(ASPECT_CUSTOM_ASPECT)); + adminService.makeCustomisable(ASPECT_CUSTOM_ASPECT); + madeCustomisable.add(ASPECT_CUSTOM_ASPECT); + assertTrue(adminService.isCustomisable(ASPECT_CUSTOM_ASPECT)); + + // Add a custom property + return adminService.addCustomPropertyDefinition( + QName.createQName(RecordsManagementCustomModel.RM_CUSTOM_URI, "myNewAspectProperty"), + ASPECT_CUSTOM_ASPECT, + "Label", + DataTypeDefinition.TEXT, + "Title", + "Description"); + } + + @Override + public void test(QName result) throws Exception + { + // Check the property QName is correct + assertNotNull(result); + assertEquals(QName.createQName(RecordsManagementCustomModel.RM_CUSTOM_URI, "myNewAspectProperty"), result); + assertTrue(adminService.existsCustomProperty(result)); + + // Check that property is available as a custom property + Map propDefs = adminService.getCustomPropertyDefinitions(ASPECT_CUSTOM_ASPECT); + assertNotNull(propDefs); + assertEquals(1, propDefs.size()); + assertTrue(propDefs.containsKey(result)); + + // Check the property definition + PropertyDefinition propDef = propDefs.get(result); + assertNotNull(propDef); + assertEquals(DataTypeDefinition.TEXT, propDef.getDataType().getName()); + assertEquals("Description", propDef.getDescription()); + assertEquals("Label", propDef.getTitle()); + } + }); + } + + public void testUseCustomProperty() throws Exception + { + // Create custom property on type and aspect + doTestInTransaction(new Test() + { + @Override + public QName run() throws Exception + { + adminService.makeCustomisable(TYPE_CUSTOM_TYPE); + madeCustomisable.add(TYPE_CUSTOM_TYPE); + adminService.addCustomPropertyDefinition( + QName.createQName(RecordsManagementCustomModel.RM_CUSTOM_URI, "myNewProperty"), + TYPE_CUSTOM_TYPE, + "Label", + DataTypeDefinition.TEXT, + "Title", + "Description"); + adminService.makeCustomisable(ASPECT_CUSTOM_ASPECT); + madeCustomisable.add(ASPECT_CUSTOM_ASPECT); + adminService.addCustomPropertyDefinition( + QName.createQName(RecordsManagementCustomModel.RM_CUSTOM_URI, "myNewAspectProperty"), + ASPECT_CUSTOM_ASPECT, + "Label", + DataTypeDefinition.TEXT, + "Title", + "Description"); + + return null; + } + }); + + // Create nodes using custom type and aspect + doTestInTransaction(new Test() + { + @Override + public QName run() throws Exception + { + NodeRef customInstance1 = nodeService.createNode( + folder, + ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "myCustomInstance1"), + TYPE_CUSTOM_TYPE).getChildRef(); + NodeRef customInstance2 = nodeService.createNode( + folder, + ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "myCustomInstance2"), + TYPE_CONTENT).getChildRef(); + nodeService.addAspect(customInstance2, ASPECT_CUSTOM_ASPECT, null); + + // Assert that both instances have the custom aspects applied + assertTrue(nodeService.hasAspect(customInstance1, QName.createQName(RM_CUSTOM_URI, "rmtcustomTypeCustomProperties"))); + assertTrue(nodeService.hasAspect(customInstance2, QName.createQName(RM_CUSTOM_URI, "rmtcustomAspectCustomProperties"))); + + // Remove the custom aspect + nodeService.removeAspect(customInstance2, ASPECT_CUSTOM_ASPECT); + + // Assert the custom property aspect is no longer applied applied + assertTrue(nodeService.hasAspect(customInstance1, QName.createQName(RM_CUSTOM_URI, "rmtcustomTypeCustomProperties"))); + assertFalse(nodeService.hasAspect(customInstance2, QName.createQName(RM_CUSTOM_URI, "rmtcustomAspectCustomProperties"))); + + return null; + } + }); + } + + + public void testCreateAndUseCustomChildReference() throws Exception + { + long now = System.currentTimeMillis(); + createAndUseCustomReference(CustomReferenceType.PARENT_CHILD, null, "superseded" + now, "superseding" + now); + } + + public void testCreateAndUseCustomNonChildReference() throws Exception + { + long now = System.currentTimeMillis(); + createAndUseCustomReference(CustomReferenceType.BIDIRECTIONAL, "supporting" + now, null, null); + } + + private void createAndUseCustomReference(final CustomReferenceType refType, final String label, final String source, final String target) throws Exception + { + final NodeRef testRecord1 = retryingTransactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + NodeRef result = createRecord(rmFolder, "testRecordA" + System.currentTimeMillis()); + return result; + } + }); + final NodeRef testRecord2 = retryingTransactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + NodeRef result = createRecord(rmFolder, "testRecordB" + System.currentTimeMillis()); + return result; + } + }); + + final QName generatedQName = retryingTransactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public QName execute() throws Throwable + { + declareRecord(testRecord1); + declareRecord(testRecord2); + + Map params = new HashMap(); + params.put("referenceType", refType.toString()); + if (label != null) params.put("label", label); + if (source != null) params.put("source", source); + if (target != null) params.put("target", target); + + // Create the reference definition. + QName qNameResult; + if (label != null) + { + // A bidirectional reference + qNameResult = adminService.addCustomAssocDefinition(label); + } + else + { + // A parent/child reference + qNameResult = adminService.addCustomChildAssocDefinition(source, target); + } + System.out.println("Creating new " + refType + " reference definition: " + qNameResult); + System.out.println(" params- label: '" + label + "' source: '" + source + "' target: '" + target + "'"); + + return qNameResult; + } + }); + + retryingTransactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + // Confirm the custom reference is included in the list from adminService. + Map customRefDefinitions = adminService.getCustomReferenceDefinitions(); + AssociationDefinition retrievedRefDefn = customRefDefinitions.get(generatedQName); + assertNotNull("Custom reference definition from adminService was null.", retrievedRefDefn); + assertEquals(generatedQName, retrievedRefDefn.getName()); + assertEquals(refType.equals(CustomReferenceType.PARENT_CHILD), retrievedRefDefn.isChild()); + + // Now we need to use the custom reference. + // So we apply the aspect containing it to our test records. + nodeService.addAspect(testRecord1, ASPECT_CUSTOM_ASSOCIATIONS, null); + + QName assocsAspectQName = QName.createQName("rmc:customAssocs", namespaceService); + nodeService.addAspect(testRecord1, assocsAspectQName, null); + + if (CustomReferenceType.PARENT_CHILD.equals(refType)) + { + nodeService.addChild(testRecord1, testRecord2, generatedQName, generatedQName); + } + else + { + nodeService.createAssociation(testRecord1, testRecord2, generatedQName); + } + return null; + } + }); + + retryingTransactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + // Read back the reference value to make sure it was correctly applied. + List childAssocs = nodeService.getChildAssocs(testRecord1); + List retrievedAssocs = nodeService.getTargetAssocs(testRecord1, RegexQNamePattern.MATCH_ALL); + + Object newlyAddedRef = null; + if (CustomReferenceType.PARENT_CHILD.equals(refType)) + { + for (ChildAssociationRef caRef : childAssocs) + { + QName refInstanceQName = caRef.getQName(); + if (generatedQName.equals(refInstanceQName)) newlyAddedRef = caRef; + } + } + else + { + for (AssociationRef aRef : retrievedAssocs) + { + QName refQName = aRef.getTypeQName(); + if (generatedQName.equals(refQName)) newlyAddedRef = aRef; + } + } + assertNotNull("newlyAddedRef was null.", newlyAddedRef); + + // Check that the reference has appeared in the data dictionary + AspectDefinition customAssocsAspect = dictionaryService.getAspect(ASPECT_CUSTOM_ASSOCIATIONS); + assertNotNull(customAssocsAspect); + if (CustomReferenceType.PARENT_CHILD.equals(refType)) + { + assertNotNull("The customReference is not returned from the dictionaryService.", + customAssocsAspect.getChildAssociations().get(generatedQName)); + } + else + { + assertNotNull("The customReference is not returned from the dictionaryService.", + customAssocsAspect.getAssociations().get(generatedQName)); + } + return null; + } + }); + } + + public void testGetAllProperties() + { + retryingTransactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + // Just dump them out for visual inspection + System.out.println("Available custom properties:"); + Map props = adminService.getCustomPropertyDefinitions(); + for (QName prop : props.keySet()) + { + System.out.println(" - " + prop.toString()); + + String propId = props.get(prop).getTitle(); + assertNotNull("null client-id for " + prop, propId); + + System.out.println(" " + propId); + } + return null; + } + }); + } + + public void testGetAllReferences() + { + retryingTransactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + // Just dump them out for visual inspection + System.out.println("Available custom references:"); + Map references = adminService.getCustomReferenceDefinitions(); + for (QName reference : references.keySet()) + { + System.out.println(" - " + reference.toString()); + System.out.println(" " + references.get(reference).getTitle()); + } + return null; + } + }); + } + + public void testGetAllConstraints() + { + retryingTransactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + // Just dump them out for visual inspection + System.out.println("Available custom constraints:"); + List constraints = adminService.getCustomConstraintDefinitions(RecordsManagementCustomModel.RM_CUSTOM_MODEL); + for (ConstraintDefinition constraint : constraints) + { + System.out.println(" - " + constraint.getName()); + System.out.println(" " + constraint.getTitle()); + } + return null; + } + }); + } + + private boolean beforeMarker = false; + private boolean onMarker = false; + @SuppressWarnings("unused") + private boolean inTest = false; + + public void testCreateReference() throws Exception + { + inTest = true; + try + { + // Create the necessary test objects in the db: two records. + final Pair testRecords = retryingTransactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback>() + { + public Pair execute() throws Throwable + { + NodeRef rec1 = createRecord(rmFolder, "testRecordA" + System.currentTimeMillis()); + NodeRef rec2 = createRecord(rmFolder, "testRecordB" + System.currentTimeMillis()); + Pair result = new Pair(rec1, rec2); + return result; + } + }); + final NodeRef testRecord1 = testRecords.getFirst(); + final NodeRef testRecord2 = testRecords.getSecond(); + + retryingTransactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + declareRecord(testRecord1); + declareRecord(testRecord2); + + policyComponent.bindClassBehaviour( + RecordsManagementPolicies.BEFORE_CREATE_REFERENCE, + this, + new JavaBehaviour(RecordsManagementAdminServiceImplTest.this, "beforeCreateReference", NotificationFrequency.EVERY_EVENT)); + policyComponent.bindClassBehaviour( + RecordsManagementPolicies.ON_CREATE_REFERENCE, + this, + new JavaBehaviour(RecordsManagementAdminServiceImplTest.this, "onCreateReference", NotificationFrequency.EVERY_EVENT)); + + assertFalse(beforeMarker); + assertFalse(onMarker); + + adminService.addCustomReference(testRecord1, testRecord2, CUSTOM_REF_VERSIONS); + + assertTrue(beforeMarker); + assertTrue(onMarker); + return null; + } + }); + } + finally + { + inTest = false; + } + } + + public void beforeCreateReference(NodeRef fromNodeRef, NodeRef toNodeRef, QName reference) + { + beforeMarker = true; + } + + public void onCreateReference(NodeRef fromNodeRef, NodeRef toNodeRef, QName reference) + { + onMarker = true; + } + + public void testCreateCustomConstraints() throws Exception + { + final int beforeCnt = + retryingTransactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Integer execute() throws Throwable + { + List result = adminService.getCustomConstraintDefinitions(RecordsManagementCustomModel.RM_CUSTOM_MODEL); + assertNotNull(result); + return result.size(); + } + }); + + final String conTitle = "test title - "+testRunID; + final List allowedValues = new ArrayList(3); + allowedValues.add("RED"); + allowedValues.add("AMBER"); + allowedValues.add("GREEN"); + + final QName testCon = retryingTransactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public QName execute() throws Throwable + { + String conLocalName = "test-"+testRunID; + + final QName result = QName.createQName(RecordsManagementCustomModel.RM_CUSTOM_URI, conLocalName); + + adminService.addCustomConstraintDefinition(result, conTitle, true, allowedValues, MatchLogic.AND); + return result; + } + }); + + + // Set the current security context as System - to see allowed values (unless caveat config is also updated for admin) + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + + retryingTransactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + List customConstraintDefs = adminService.getCustomConstraintDefinitions(RecordsManagementCustomModel.RM_CUSTOM_MODEL); + assertEquals(beforeCnt+1, customConstraintDefs.size()); + + boolean found = false; + for (ConstraintDefinition conDef : customConstraintDefs) + { + if (conDef.getName().equals(testCon)) + { + assertEquals(conTitle, conDef.getTitle()); + + Constraint con = conDef.getConstraint(); + assertTrue(con instanceof RMListOfValuesConstraint); + + assertEquals("LIST", ((RMListOfValuesConstraint)con).getType()); + assertEquals(3, ((RMListOfValuesConstraint)con).getAllowedValues().size()); + + found = true; + break; + } + } + assertTrue(found); + return null; + } + }); + + + // Set the current security context as admin + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + + retryingTransactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + allowedValues.clear(); + allowedValues.add("RED"); + allowedValues.add("YELLOW"); + + adminService.changeCustomConstraintValues(testCon, allowedValues); + return null; + } + }); + + // Set the current security context as System - to see allowed values (unless caveat config is also updated for admin) + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + + retryingTransactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + List customConstraintDefs = adminService.getCustomConstraintDefinitions(RecordsManagementCustomModel.RM_CUSTOM_MODEL); + assertEquals(beforeCnt+1, customConstraintDefs.size()); + + boolean found = false; + for (ConstraintDefinition conDef : customConstraintDefs) + { + if (conDef.getName().equals(testCon)) + { + assertEquals(conTitle, conDef.getTitle()); + + Constraint con = conDef.getConstraint(); + assertTrue(con instanceof RMListOfValuesConstraint); + + assertEquals("LIST", ((RMListOfValuesConstraint)con).getType()); + assertEquals(2, ((RMListOfValuesConstraint)con).getAllowedValues().size()); + + found = true; + break; + } + } + assertTrue(found); + return null; + } + }); + + + // Set the current security context as admin + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + + // Add custom property to record with test constraint + retryingTransactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + String propLocalName = "myProp-"+testRunID; + + QName dataType = DataTypeDefinition.TEXT; + String propTitle = "My property title"; + String description = "My property description"; + String defaultValue = null; + boolean multiValued = false; + boolean mandatory = false; + boolean isProtected = false; + + QName propName = adminService.addCustomPropertyDefinition(null, ASPECT_RECORD, propLocalName, dataType, propTitle, description, defaultValue, multiValued, mandatory, isProtected, testCon); + createdCustomProperties.add(propName); + return null; + } + }); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementAuditServiceImplTest.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementAuditServiceImplTest.java new file mode 100644 index 0000000000..3d2ec5cd27 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementAuditServiceImplTest.java @@ -0,0 +1,427 @@ +/* + * 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.module.org_alfresco_module_rm.test.service; + +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import junit.framework.TestCase; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.audit.RecordsManagementAuditEntry; +import org.alfresco.module.org_alfresco_module_rm.audit.RecordsManagementAuditQueryParameters; +import org.alfresco.module.org_alfresco_module_rm.audit.RecordsManagementAuditService; +import org.alfresco.module.org_alfresco_module_rm.test.util.TestUtilities; +import org.alfresco.repo.security.authentication.AuthenticationException; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.security.MutableAuthenticationService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ApplicationContextHelper; +import org.alfresco.util.EqualsHelper; +import org.springframework.context.ApplicationContext; + +/** + * @see RecordsManagementAuditService + * + * @author Derek Hulley + * @since 3.2 + */ +public class RecordsManagementAuditServiceImplTest extends TestCase +{ + private ApplicationContext ctx; + + private ServiceRegistry serviceRegistry; + private NodeService nodeService; + private TransactionService transactionService; + private RetryingTransactionHelper txnHelper; + private RecordsManagementAuditService rmAuditService; + + + private Date testStartTime; + private NodeRef filePlan; + + @Override + protected void setUp() throws Exception + { + testStartTime = new Date(); + + // We require that records management auditing is enabled + // This gets done by the AMP, but as we're not running from + // and AMP, we need to do it ourselves! + System.setProperty("audit.rm.enabled", "true"); + + // Now we can fetch the context + ctx = ApplicationContextHelper.getApplicationContext(); + + this.serviceRegistry = (ServiceRegistry) ctx.getBean(ServiceRegistry.SERVICE_REGISTRY); + this.transactionService = serviceRegistry.getTransactionService(); + this.txnHelper = transactionService.getRetryingTransactionHelper(); + + this.rmAuditService = (RecordsManagementAuditService) ctx.getBean("RecordsManagementAuditService"); + + this.nodeService = serviceRegistry.getNodeService(); + + + // Set the current security context as admin + AuthenticationUtil.setRunAsUser(AuthenticationUtil.getSystemUserName()); + + // Stop and clear the log + rmAuditService.stop(); + rmAuditService.clear(); + rmAuditService.start(); + + RetryingTransactionCallback setUpCallback = new RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + if (filePlan == null) + { + filePlan = TestUtilities.loadFilePlanData(ctx); + } + updateFilePlan(); + return null; + } + }; + txnHelper.doInTransaction(setUpCallback); + } + + @Override + protected void tearDown() + { + AuthenticationUtil.clearCurrentSecurityContext(); + try + { + rmAuditService.start(); + } + catch (Throwable e) + { + // Not too important + } + } + + /** + * Perform a full query audit for RM + * @return Returns all the results + */ + private List queryAll() + { + RetryingTransactionCallback> testCallback = + new RetryingTransactionCallback>() + { + public List execute() throws Throwable + { + RecordsManagementAuditQueryParameters params = new RecordsManagementAuditQueryParameters(); + List entries = rmAuditService.getAuditTrail(params); + return entries; + } + }; + return txnHelper.doInTransaction(testCallback); + } + + /** + * Create a new fileplan + */ + private void updateFilePlan() + { + RetryingTransactionCallback updateCallback = new RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + // Do some stuff + nodeService.setProperty(filePlan, ContentModel.PROP_TITLE, "File Plan - " + System.currentTimeMillis()); + + return null; + } + }; + txnHelper.doInTransaction(updateCallback); + } + + public void testSetUp() + { + // Just to get get the fileplan set up + } + + public void testQuery_All() + { + queryAll(); + } + + public void testQuery_UserLimited() + { + // Make sure that something has been done + updateFilePlan(); + + final int limit = 1; + final String user = AuthenticationUtil.getSystemUserName(); // The user being tested + + RetryingTransactionCallback> testCallback = + new RetryingTransactionCallback>() + { + public List execute() throws Throwable + { + RecordsManagementAuditQueryParameters params = new RecordsManagementAuditQueryParameters(); + params.setUser(user); + params.setMaxEntries(limit); + List entries = rmAuditService.getAuditTrail(params); + return entries; + } + }; + List entries = txnHelper.doInTransaction(testCallback); + assertNotNull(entries); + assertEquals("Expected results to be limited", limit, entries.size()); + } + + public void testQuery_Node() throws InterruptedException + { + RetryingTransactionCallback> allResultsCallback = + new RetryingTransactionCallback>() + { + public List execute() throws Throwable + { + RecordsManagementAuditQueryParameters params = new RecordsManagementAuditQueryParameters(); + params.setDateFrom(testStartTime); + List entries = rmAuditService.getAuditTrail(params); + return entries; + } + }; + List entries = txnHelper.doInTransaction(allResultsCallback); + assertNotNull("Expect a list of results for the query", entries); + + // Find all results for a given node + NodeRef chosenNodeRef = null; + int count = 0; + for (RecordsManagementAuditEntry entry : entries) + { + NodeRef nodeRef = entry.getNodeRef(); + assertNotNull("Found entry with null nodeRef: " + entry, nodeRef); + if (chosenNodeRef == null) + { + chosenNodeRef = nodeRef; + count++; + } + else if (nodeRef.equals(chosenNodeRef)) + { + count++; + } + } + + final NodeRef chosenNodeRefFinal = chosenNodeRef; + // Now search again, but for the chosen node + RetryingTransactionCallback> nodeResultsCallback = + new RetryingTransactionCallback>() + { + public List execute() throws Throwable + { + RecordsManagementAuditQueryParameters params = new RecordsManagementAuditQueryParameters(); + params.setDateFrom(testStartTime); + params.setNodeRef(chosenNodeRefFinal); + List entries = rmAuditService.getAuditTrail(params); + return entries; + } + }; + entries = txnHelper.doInTransaction(nodeResultsCallback); + assertNotNull("Expect a list of results for the query", entries); + assertTrue("No results were found for node: " + chosenNodeRefFinal, entries.size() > 0); + // We can't check the size because we need entries for the node and any children as well + + Thread.sleep(5000); + + // Clear the log + rmAuditService.clear(); + + entries = txnHelper.doInTransaction(nodeResultsCallback); + assertTrue("Should have cleared all audit entries", entries.isEmpty()); + + // Delete the node + txnHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + return AuthenticationUtil.runAs(new RunAsWork() + { + @Override + public Void doWork() throws Exception + { + nodeService.deleteNode(chosenNodeRefFinal); + return null; + } + }, AuthenticationUtil.getSystemUserName()); + } + }); + + Thread.sleep(5000); + + entries = txnHelper.doInTransaction(nodeResultsCallback); + assertFalse("Should have recorded node deletion", entries.isEmpty()); + } + + public void testStartStopDelete() throws InterruptedException + { + // Stop the audit + rmAuditService.stop(); + + Thread.sleep(5000); + + List result1 = queryAll(); + assertNotNull(result1); + + // Update the fileplan + updateFilePlan(); + + Thread.sleep(5000); + + // There should be no new audit entries + List result2 = queryAll(); + assertNotNull(result2); + assertEquals( + "Audit results should not have changed after auditing was disabled", + result1.size(), result2.size()); + + // repeat with a start + rmAuditService.start(); + updateFilePlan(); + + Thread.sleep(5000); + + List result3 = queryAll(); + assertNotNull(result3); + assertTrue( + "Expected more results after enabling audit", + result3.size() > result1.size()); + + Thread.sleep(5000); + + // Stop and delete all entries + rmAuditService.stop(); + rmAuditService.clear(); + + // There should be no entries + List result4 = queryAll(); + assertNotNull(result4); + assertEquals( + "Audit entries should have been cleared", + 0, result4.size()); + } + + public void xtestAuditAuthentication() + { + rmAuditService.stop(); + rmAuditService.clear(); + rmAuditService.start(); + + MutableAuthenticationService authenticationService = serviceRegistry.getAuthenticationService(); + PersonService personService = serviceRegistry.getPersonService(); + + try + { + personService.deletePerson("baboon"); + authenticationService.deleteAuthentication("baboon"); + } + catch (Throwable e) + { + // Not serious + } + + // Failed login attempt ... + try + { + AuthenticationUtil.pushAuthentication(); + authenticationService.authenticate("baboon", "lskdfj".toCharArray()); + fail("Expected authentication failure"); + } + catch (AuthenticationException e) + { + // Good + } + finally + { + AuthenticationUtil.popAuthentication(); + } + rmAuditService.stop(); + List result1 = queryAll(); + // Check that the username is reflected correctly in the results + assertFalse("No audit results were generated for the failed login.", result1.isEmpty()); + boolean found = false; + for (RecordsManagementAuditEntry entry : result1) + { + String userName = entry.getUserName(); + if (userName.equals("baboon")) + { + found = true; + break; + } + } + assertTrue("Expected to hit failed login attempt for user", found); + + // Test successful authentication + try + { + personService.deletePerson("cdickons"); + authenticationService.deleteAuthentication("cdickons"); + } + catch (Throwable e) + { + // Not serious + } + authenticationService.createAuthentication("cdickons", getName().toCharArray()); + Map personProperties = new HashMap(); + personProperties.put(ContentModel.PROP_USERNAME, "cdickons"); + personProperties.put(ContentModel.PROP_FIRSTNAME, "Charles"); + personProperties.put(ContentModel.PROP_LASTNAME, "Dickons"); + personService.createPerson(personProperties); + + rmAuditService.clear(); + rmAuditService.start(); + try + { + AuthenticationUtil.pushAuthentication(); + authenticationService.authenticate("cdickons", getName().toCharArray()); + } + finally + { + AuthenticationUtil.popAuthentication(); + } + rmAuditService.stop(); + List result2 = queryAll(); + found = false; + for (RecordsManagementAuditEntry entry : result2) + { + String userName = entry.getUserName(); + String fullName = entry.getFullName(); + if (userName.equals("cdickons") && EqualsHelper.nullSafeEquals(fullName, "Charles Dickons")) + { + found = true; + break; + } + } + assertTrue("Expected to hit successful login attempt for Charles Dickons (cdickons)", found); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementEventServiceImplTest.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementEventServiceImplTest.java new file mode 100644 index 0000000000..1bfecc00c8 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementEventServiceImplTest.java @@ -0,0 +1,139 @@ +/* + * 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.module.org_alfresco_module_rm.test.service; + +import java.util.List; + +import org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEvent; +import org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEventService; +import org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEventType; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.util.BaseSpringTest; + +/** + * Event service implementation unit test + * + * @author Roy Wetherall + */ +public class RecordsManagementEventServiceImplTest extends BaseSpringTest implements RecordsManagementModel +{ + protected static StoreRef SPACES_STORE = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "SpacesStore"); + + private RecordsManagementEventService rmEventService; + private RetryingTransactionHelper transactionHelper; + + @Override + protected void onSetUpInTransaction() throws Exception + { + super.onSetUpInTransaction(); + + // Get the service required in the tests + this.rmEventService = (RecordsManagementEventService)this.applicationContext.getBean("RecordsManagementEventService"); + this.transactionHelper = (RetryingTransactionHelper)this.applicationContext.getBean("retryingTransactionHelper"); + + // Set the current security context as admin + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + } + + public void testGetEventTypes() + { + setComplete(); + endTransaction(); + + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + List eventTypes = rmEventService.getEventTypes(); + assertNotNull(eventTypes); + for (RecordsManagementEventType eventType : eventTypes) + { + System.out.println(eventType.getName() + " - " + eventType.getDisplayLabel()); + } + return null; + } + }); + } + + public void testGetEvents() + { + setComplete(); + endTransaction(); + + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + List events = rmEventService.getEvents(); + assertNotNull(events); + for (RecordsManagementEvent event : events) + { + System.out.println(event.getName()); + } + return null; + } + }); + } + + public void testAddRemoveEvents() + { + setComplete(); + endTransaction(); + + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + List events = rmEventService.getEvents(); + assertNotNull(events); + assertFalse(containsEvent(events, "myEvent")); + + rmEventService.addEvent("rmEventType.simple", "myEvent", "My Event"); + + events = rmEventService.getEvents(); + assertNotNull(events); + assertTrue(containsEvent(events, "myEvent")); + + rmEventService.removeEvent("myEvent"); + + events = rmEventService.getEvents(); + assertNotNull(events); + assertFalse(containsEvent(events, "myEvent")); + return null; + } + }); + } + + private boolean containsEvent(List events, String eventName) + { + boolean result = false; + for (RecordsManagementEvent event : events) + { + if (eventName.equals(event.getName()) == true) + { + result = true; + break; + } + } + return result; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementSearchServiceImplTest.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementSearchServiceImplTest.java new file mode 100644 index 0000000000..8ad3c91911 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementSearchServiceImplTest.java @@ -0,0 +1,311 @@ +/* + * 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.module.org_alfresco_module_rm.test.service; + +import java.util.List; + +import org.alfresco.module.org_alfresco_module_rm.search.RecordsManagementSearchParameters; +import org.alfresco.module.org_alfresco_module_rm.search.SavedSearchDetails; +import org.alfresco.module.org_alfresco_module_rm.test.util.BaseRMTestCase; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.MutableAuthenticationService; +import org.alfresco.util.TestWithUserUtils; + +/** + * Search service implementation unit test. + * + * @author Roy Wetherall + */ +public class RecordsManagementSearchServiceImplTest extends BaseRMTestCase +{ + @Override + protected boolean isMultiHierarchyTest() + { + return true; + } + + private static final String SEARCH1 = "search1"; + private static final String SEARCH2 = "search2"; + private static final String SEARCH3 = "search3"; + private static final String SEARCH4 = "search4"; + + private static final String USER1 = "user1"; + private static final String USER2 = "user2"; + + private NodeRef folderLevelRecordFolder; + private NodeRef recordLevelRecordFolder; + + private NodeRef recordOne; + private NodeRef recordTwo; + private NodeRef recordThree; + private NodeRef recordFour; + private NodeRef recordFive; + private NodeRef recordSix; + + private MutableAuthenticationService authenticationService; + + private int numberOfReports; + + /** + * @see org.alfresco.module.org_alfresco_module_rm.test.util.BaseRMTestCase#setupTestData() + */ + @Override + protected void setupTestData() + { + super.setupTestData(); + + authenticationService = (MutableAuthenticationService)applicationContext.getBean("AuthenticationService"); + + doTestInTransaction(new Test() + { + @Override + public Void run() + { + // Create test users + TestWithUserUtils.createUser(USER1, USER1, rootNodeRef, nodeService, authenticationService); + TestWithUserUtils.createUser(USER2, USER2, rootNodeRef, nodeService, authenticationService); + + // Count the number of pre-defined reports + List searches = rmSearchService.getSavedSearches(SITE_ID); + assertNotNull(searches); + numberOfReports = searches.size(); + + return null; + } + }); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.test.util.BaseRMTestCase#setupMultiHierarchyTestData() + */ + @Override + protected void setupMultiHierarchyTestData() + { + super.setupMultiHierarchyTestData(); + + doTestInTransaction(new Test() + { + @Override + public Void run() + { + folderLevelRecordFolder = mhRecordFolder42; + recordLevelRecordFolder = mhRecordFolder43; + + recordOne = createRecord(folderLevelRecordFolder, "recordOne.txt", null, "record one - folder level - elephant"); + recordTwo = createRecord(folderLevelRecordFolder, "recordTwo.txt", null, "record two - folder level - snake"); + recordThree = createRecord(folderLevelRecordFolder, "recordThree.txt", null, "record three - folder level - monkey"); + recordFour = createRecord(recordLevelRecordFolder, "recordFour.txt", null, "record four - record level - elephant"); + recordFive = createRecord(recordLevelRecordFolder, "recordFive.txt", null, "record five - record level - snake"); + recordSix = createRecord(recordLevelRecordFolder, "recordSix.txt", null, "record six - record level - monkey"); + + return null; + } + }); + } + + @Override + protected void tearDown() throws Exception + { + super.tearDown(); + + doTestInTransaction(new Test() + { + @Override + public Void run() + { + // Delete test users + TestWithUserUtils.deleteUser(USER1, USER1, rootNodeRef, nodeService, authenticationService); + TestWithUserUtils.deleteUser(USER2, USER2, rootNodeRef, nodeService, authenticationService); + + return null; + } + }); + } + + public void testSearch() + { + // Full text search + doTestInTransaction(new Test() + { + @Override + public Void run() + { + String query = "keywords:\"elephant\""; + RecordsManagementSearchParameters params = new RecordsManagementSearchParameters(); + params.setIncludeUndeclaredRecords(true); + List results = rmSearchService.search(SITE_ID, query, params); + assertNotNull(results); + assertEquals(2, results.size()); + + return null; + } + }); + + // Property search + + // + } + + public void testSaveSearch() + { + // Add some saved searches (as admin user) + doTestInTransaction(new Test() + { + @Override + public Void run() + { + SavedSearchDetails details1 = rmSearchService.saveSearch(SITE_ID, SEARCH1, "description1", "query1", new RecordsManagementSearchParameters(), true); + checkSearchDetails(details1, "mySite", "search1", "description1", "query1", new RecordsManagementSearchParameters(), true); + SavedSearchDetails details2 = rmSearchService.saveSearch(SITE_ID, SEARCH2, "description2", "query2", new RecordsManagementSearchParameters(), false); + checkSearchDetails(details2, "mySite", "search2", "description2", "query2", new RecordsManagementSearchParameters(), false); + + return null; + } + + }); + + // Add some saved searches (as user1) + doTestInTransaction(new Test() + { + @Override + public Void run() + { + SavedSearchDetails details1 = rmSearchService.saveSearch(SITE_ID, SEARCH3, "description3", "query3", new RecordsManagementSearchParameters(), false); + checkSearchDetails(details1, "mySite", SEARCH3, "description3", "query3", new RecordsManagementSearchParameters(), false); + SavedSearchDetails details2 = rmSearchService.saveSearch(SITE_ID, SEARCH4, "description4", "query4", new RecordsManagementSearchParameters(), false); + checkSearchDetails(details2, "mySite", SEARCH4, "description4", "query4", new RecordsManagementSearchParameters(), false); + + return null; + } + + }, USER1); + + // Get searches (as admin user) + doTestInTransaction(new Test() + { + @Override + public Void run() + { + List searches = rmSearchService.getSavedSearches(SITE_ID); + assertNotNull(searches); + assertEquals(numberOfReports + 2, searches.size()); + + SavedSearchDetails search1 = rmSearchService.getSavedSearch(SITE_ID, SEARCH1); + assertNotNull(search1); + checkSearchDetails(search1, "mySite", "search1", "description1", "query1", new RecordsManagementSearchParameters(), true); + + SavedSearchDetails search2 = rmSearchService.getSavedSearch(SITE_ID, SEARCH2); + assertNotNull(search2); + checkSearchDetails(search2, "mySite", "search2", "description2", "query2", new RecordsManagementSearchParameters(), false); + + SavedSearchDetails search3 = rmSearchService.getSavedSearch(SITE_ID, SEARCH3); + assertNull(search3); + + SavedSearchDetails search4 = rmSearchService.getSavedSearch(SITE_ID, SEARCH4); + assertNull(search4); + + return null; + } + + }); + + // Get searches (as user1) + doTestInTransaction(new Test() + { + @Override + public Void run() + { + List searches = rmSearchService.getSavedSearches(SITE_ID); + assertNotNull(searches); + assertEquals(numberOfReports + 3, searches.size()); + + SavedSearchDetails search1 = rmSearchService.getSavedSearch(SITE_ID, SEARCH1); + assertNotNull(search1); + checkSearchDetails(search1, "mySite", "search1", "description1", "query1", new RecordsManagementSearchParameters(), true); + + SavedSearchDetails search2 = rmSearchService.getSavedSearch(SITE_ID, SEARCH2); + assertNull(search2); + + SavedSearchDetails search3 = rmSearchService.getSavedSearch(SITE_ID, SEARCH3); + assertNotNull(search3); + checkSearchDetails(search3, "mySite", SEARCH3, "description3", "query3", new RecordsManagementSearchParameters(), false); + + SavedSearchDetails search4 = rmSearchService.getSavedSearch(SITE_ID, SEARCH4); + assertNotNull(search4); + checkSearchDetails(search4, "mySite", "search4", "description4", "query4", new RecordsManagementSearchParameters(), false); + + return null; + } + + }, USER1); + + // Update search (as admin user) + doTestInTransaction(new Test() + { + @Override + public Void run() + { + SavedSearchDetails search1 = rmSearchService.getSavedSearch(SITE_ID, SEARCH1); + assertNotNull(search1); + checkSearchDetails(search1, SITE_ID, SEARCH1, "description1", "query1", new RecordsManagementSearchParameters(), true); + + rmSearchService.saveSearch(SITE_ID, SEARCH1, "change", "change", new RecordsManagementSearchParameters(), true); + + search1 = rmSearchService.getSavedSearch(SITE_ID, SEARCH1); + assertNotNull(search1); + checkSearchDetails(search1, SITE_ID, SEARCH1, "change", "change", new RecordsManagementSearchParameters(), true); + + return null; + } + }); + + // Delete searches (as admin user) + // TODO + } + + /** + * Check the details of the saved search. + */ + private void checkSearchDetails( + SavedSearchDetails details, + String siteid, + String name, + String description, + String query, + RecordsManagementSearchParameters searchParameters, + boolean isPublic) + { + assertNotNull(details); + assertEquals(siteid, details.getSiteId()); + assertEquals(name, details.getName()); + assertEquals(description, details.getDescription()); + assertEquals(query, details.getSearch()); + assertEquals(isPublic, details.isPublic()); + + assertEquals(searchParameters.getMaxItems(), details.getSearchParameters().getMaxItems()); + assertEquals(searchParameters.isIncludeRecords(), details.getSearchParameters().isIncludeRecords()); + assertEquals(searchParameters.isIncludeUndeclaredRecords(), details.getSearchParameters().isIncludeUndeclaredRecords()); + assertEquals(searchParameters.isIncludeVitalRecords(), details.getSearchParameters().isIncludeVitalRecords()); + assertEquals(searchParameters.isIncludeRecordFolders(), details.getSearchParameters().isIncludeRecordFolders()); + assertEquals(searchParameters.isIncludeFrozen(), details.getSearchParameters().isIncludeFrozen()); + assertEquals(searchParameters.isIncludeCutoff(), details.getSearchParameters().isIncludeCutoff()); + + // Check the other stuff .... + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementSecurityServiceImplTest.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementSecurityServiceImplTest.java new file mode 100644 index 0000000000..cc414805c0 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementSecurityServiceImplTest.java @@ -0,0 +1,692 @@ +/* + * 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.module.org_alfresco_module_rm.test.service; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService; +import org.alfresco.module.org_alfresco_module_rm.capability.Capability; +import org.alfresco.module.org_alfresco_module_rm.capability.RMPermissionModel; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService; +import org.alfresco.module.org_alfresco_module_rm.security.Role; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +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.cmr.security.AccessPermission; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.AuthorityService; +import org.alfresco.service.cmr.security.AuthorityType; +import org.alfresco.service.cmr.security.MutableAuthenticationService; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.BaseSpringTest; +import org.alfresco.util.GUID; +import org.alfresco.util.PropertyMap; + +/** + * Event service implementation unit test + * + * @author Roy Wetherall + */ +public class RecordsManagementSecurityServiceImplTest extends BaseSpringTest + implements RecordsManagementModel +{ + protected static StoreRef SPACES_STORE = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "SpacesStore"); + + private NodeService nodeService; + private MutableAuthenticationService authenticationService; + private AuthorityService authorityService; + private PermissionService permissionService; + private PersonService personService; + private RecordsManagementSecurityService rmSecurityService; + private RecordsManagementActionService rmActionService; + private RetryingTransactionHelper transactionHelper; + + @Override + protected void onSetUpInTransaction() throws Exception + { + super.onSetUpInTransaction(); + + // Get the service required in the tests + this.nodeService = (NodeService)this.applicationContext.getBean("NodeService"); + this.authenticationService = (MutableAuthenticationService)this.applicationContext.getBean("AuthenticationService"); + this.personService = (PersonService)this.applicationContext.getBean("PersonService"); + this.authorityService = (AuthorityService)this.applicationContext.getBean("authorityService"); + this.rmSecurityService = (RecordsManagementSecurityService)this.applicationContext.getBean("RecordsManagementSecurityService"); + this.transactionHelper = (RetryingTransactionHelper)this.applicationContext.getBean("retryingTransactionHelper"); + this.permissionService = (PermissionService)this.applicationContext.getBean("PermissionService"); + this.rmActionService = (RecordsManagementActionService)this.applicationContext.getBean("RecordsManagementActionService"); + + // Set the current security context as admin + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + } + + public void testRoles() + { + final NodeRef rmRootNode = createRMRootNodeRef(); + + setComplete(); + endTransaction(); + + transactionHelper.doInTransaction(new RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + Set roles = rmSecurityService.getRoles(rmRootNode); + assertNotNull(roles); + assertEquals(5, roles.size()); + + rmSecurityService.createRole(rmRootNode, "MyRole", "My Role", getListOfCapabilities(5)); + + roles = rmSecurityService.getRoles(rmRootNode); + assertNotNull(roles); + assertEquals(6, roles.size()); + + Role role = findRole(roles, "MyRole"); + assertNotNull(role); + assertEquals("MyRole", role.getName()); + assertEquals("My Role", role.getDisplayLabel()); + assertNotNull(role.getCapabilities()); + assertEquals(5, role.getCapabilities().size()); + assertNotNull(role.getRoleGroupName()); + + // Add a user to the role + String userName = createAndAddUserToRole(role.getRoleGroupName()); + + // Check that we can retrieve the users roles + Set userRoles = rmSecurityService.getRolesByUser(rmRootNode, userName); + assertNotNull(userRoles); + assertEquals(1, userRoles.size()); + Role userRole = userRoles.iterator().next(); + assertEquals("MyRole", userRole.getName()); + + try + { + rmSecurityService.createRole(rmRootNode, "MyRole", "My Role", getListOfCapabilities(5)); + fail("Duplicate role id's not allowed for the same rm root node"); + } + catch (AlfrescoRuntimeException e) + { + // Expected + } + + rmSecurityService.createRole(rmRootNode, "MyRole2", "My Role", getListOfCapabilities(5)); + + roles = rmSecurityService.getRoles(rmRootNode); + assertNotNull(roles); + assertEquals(7, roles.size()); + + Set list = getListOfCapabilities(3, 4); + assertEquals(3, list.size()); + + Role result = rmSecurityService.updateRole(rmRootNode, "MyRole", "SomethingDifferent", list); + + assertNotNull(result); + assertEquals("MyRole", result.getName()); + assertEquals("SomethingDifferent", result.getDisplayLabel()); + assertNotNull(result.getCapabilities()); + assertEquals(3, result.getCapabilities().size()); + assertNotNull(result.getRoleGroupName()); + + roles = rmSecurityService.getRoles(rmRootNode); + assertNotNull(roles); + assertEquals(7, roles.size()); + + Role role2 = findRole(roles, "MyRole"); + assertNotNull(role2); + assertEquals("MyRole", role2.getName()); + assertEquals("SomethingDifferent", role2.getDisplayLabel()); + assertNotNull(role2.getCapabilities()); + assertEquals(3, role2.getCapabilities().size()); + assertNotNull(role2.getRoleGroupName()); + + rmSecurityService.deleteRole(rmRootNode, "MyRole2"); + + roles = rmSecurityService.getRoles(rmRootNode); + assertNotNull(roles); + assertEquals(6, roles.size()); + + return null; + } + }); + } + + private Role findRole(Set roles, String name) + { + Role result = null; + for (Role role : roles) + { + if (name.equals(role.getName()) == true) + { + result = role; + break; + } + } + + return result; + } + + private Set getListOfCapabilities(int size) + { + return getListOfCapabilities(size, 0); + } + + private Set getListOfCapabilities(int size, int offset) + { + Set result = new HashSet(size); + Set caps = rmSecurityService.getCapabilities(); + int count = 0; + for (Capability cap : caps) + { + if (count < size+offset) + { + if (count >= offset) + { + result.add(cap); + } + } + else + { + break; + } + count ++; + } + return result; + } + + private NodeRef createRMRootNodeRef() + { + NodeRef root = this.nodeService.getRootNode(SPACES_STORE); + NodeRef filePlan = this.nodeService.createNode(root, ContentModel.ASSOC_CHILDREN, ContentModel.ASSOC_CHILDREN, TYPE_FILE_PLAN).getChildRef(); + + return filePlan; + } + + private NodeRef addFilePlanCompoent(NodeRef parent, QName type) + { + String id = GUID.generate(); + String seriesName = "Series" + id; + Map props = new HashMap(2); + props.put(ContentModel.PROP_NAME, seriesName); + props.put(PROP_IDENTIFIER, id); + return nodeService.createNode( + parent, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, seriesName), + type, + props).getChildRef(); + } + + private String createAndAddUserToRole(String role) + { + // Create an athentication + String userName = GUID.generate(); + authenticationService.createAuthentication(userName, "PWD".toCharArray()); + + // Create a person + PropertyMap ppOne = new PropertyMap(4); + ppOne.put(ContentModel.PROP_USERNAME, userName); + ppOne.put(ContentModel.PROP_FIRSTNAME, "firstName"); + ppOne.put(ContentModel.PROP_LASTNAME, "lastName"); + ppOne.put(ContentModel.PROP_EMAIL, "email@email.com"); + ppOne.put(ContentModel.PROP_JOBTITLE, "jobTitle"); + personService.createPerson(ppOne); + + // Assign the new user to the role passed + authorityService.addAuthority(role, userName); + + return userName; + } + + private String createUser() + { + // Create an athentication + String userName = GUID.generate(); + authenticationService.createAuthentication(userName, "PWD".toCharArray()); + + // Create a person + PropertyMap ppOne = new PropertyMap(4); + ppOne.put(ContentModel.PROP_USERNAME, userName); + ppOne.put(ContentModel.PROP_FIRSTNAME, "firstName"); + ppOne.put(ContentModel.PROP_LASTNAME, "lastName"); + ppOne.put(ContentModel.PROP_EMAIL, "email@email.com"); + ppOne.put(ContentModel.PROP_JOBTITLE, "jobTitle"); + personService.createPerson(ppOne); + + return userName; + } + + public void testExecutionAsRMAdmin() + { + final NodeRef filePlan = createRMRootNodeRef(); + + setComplete(); + endTransaction(); + + transactionHelper.doInTransaction(new RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + System.out.println("Groups:"); + Set temp = authorityService.getAllRootAuthorities(AuthorityType.GROUP); + for (String g : temp) + { + System.out.println(" - " + g); + } + System.out.println(""); + + assertTrue(permissionService.hasPermission(filePlan, RMPermissionModel.READ_RECORDS).equals(AccessStatus.ALLOWED)); + assertTrue(permissionService.hasPermission(filePlan, RMPermissionModel.FILE_RECORDS).equals(AccessStatus.ALLOWED)); + assertTrue(permissionService.hasPermission(filePlan, RMPermissionModel.FILING).equals(AccessStatus.ALLOWED)); + + Role adminRole = rmSecurityService.getRole(filePlan, "Administrator"); + assertNotNull(adminRole); + String adminUser = createAndAddUserToRole(adminRole.getRoleGroupName()); + AuthenticationUtil.setFullyAuthenticatedUser(adminUser); + + try + { + assertTrue(permissionService.hasPermission(filePlan, RMPermissionModel.READ_RECORDS).equals(AccessStatus.ALLOWED)); + assertTrue(permissionService.hasPermission(filePlan, RMPermissionModel.FILE_RECORDS).equals(AccessStatus.ALLOWED)); + assertTrue(permissionService.hasPermission(filePlan, RMPermissionModel.FILING).equals(AccessStatus.ALLOWED)); + + // Read the properties of the filePlan + nodeService.getProperties(filePlan); + } + finally + { + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + } + + return null; + } + }); + } + + public void testDefaultRolesBootstrap() + { + NodeRef rootNode = nodeService.getRootNode(SPACES_STORE); + final NodeRef filePlan = nodeService.createNode(rootNode, ContentModel.ASSOC_CHILDREN, + TYPE_FILE_PLAN, + TYPE_FILE_PLAN).getChildRef(); + + setComplete(); + endTransaction(); + + transactionHelper.doInTransaction(new RetryingTransactionCallback() + { + + public Object execute() throws Throwable + { + Set roles = rmSecurityService.getRoles(filePlan); + assertNotNull(roles); + assertEquals(5, roles.size()); + + Role role = rmSecurityService.getRole(filePlan, "User"); + assertNotNull(role); + assertEquals("User", role.getName()); + assertNotNull(role.getDisplayLabel()); + Set caps = role.getCapabilities(); + assertNotNull(caps); + System.out.println("\nUser capabilities: "); + for (String cap : caps) + { + assertNotNull(rmSecurityService.getCapability(cap)); + System.out.println(cap); + } + + role = rmSecurityService.getRole(filePlan, "PowerUser"); + assertNotNull(role); + assertEquals("PowerUser", role.getName()); + assertNotNull(role.getDisplayLabel()); + caps = role.getCapabilities(); + assertNotNull(caps); + System.out.println("\nPowerUser capabilities: "); + for (String cap : caps) + { + assertNotNull(rmSecurityService.getCapability(cap)); + System.out.println(cap); + } + + role = rmSecurityService.getRole(filePlan, "SecurityOfficer"); + assertNotNull(role); + assertEquals("SecurityOfficer", role.getName()); + assertNotNull(role.getDisplayLabel()); + caps = role.getCapabilities(); + assertNotNull(caps); + System.out.println("\nSecurityOfficer capabilities: "); + for (String cap : caps) + { + assertNotNull(rmSecurityService.getCapability(cap)); + System.out.println(cap); + } + + role = rmSecurityService.getRole(filePlan, "RecordsManager"); + assertNotNull(role); + assertEquals("RecordsManager", role.getName()); + assertNotNull(role.getDisplayLabel()); + caps = role.getCapabilities(); + assertNotNull(caps); + System.out.println("\nRecordsManager capabilities: "); + for (String cap : caps) + { + assertNotNull(rmSecurityService.getCapability(cap)); + System.out.println(cap); + } + + role = rmSecurityService.getRole(filePlan, "Administrator"); + assertNotNull(role); + assertEquals("Administrator", role.getName()); + assertNotNull(role.getDisplayLabel()); + caps = role.getCapabilities(); + assertNotNull(caps); + System.out.println("\nAdministrator capabilities: "); + for (String cap : caps) + { + assertNotNull("No capability called " + cap, rmSecurityService.getCapability(cap)); + System.out.println(cap); + } + + return null; + } + + }); + } + + public void xtestCreateNewRMUserAccessToFilePlan() + { + final NodeRef rmRootNode = createRMRootNodeRef(); + + final NodeRef seriesOne = addFilePlanCompoent(rmRootNode, TYPE_RECORD_CATEGORY); + final NodeRef seriesTwo = addFilePlanCompoent(rmRootNode, TYPE_RECORD_CATEGORY); + final NodeRef seriesThree = addFilePlanCompoent(rmRootNode, TYPE_RECORD_CATEGORY); + + final NodeRef catOne = addFilePlanCompoent(seriesOne, TYPE_RECORD_CATEGORY); + final NodeRef catTwo = addFilePlanCompoent(seriesOne, TYPE_RECORD_CATEGORY); + final NodeRef catThree = addFilePlanCompoent(seriesOne, TYPE_RECORD_CATEGORY); + + final NodeRef folderOne = addFilePlanCompoent(catOne, TYPE_RECORD_FOLDER); + final NodeRef folderTwo = addFilePlanCompoent(catOne, TYPE_RECORD_FOLDER); + final NodeRef folderThree = addFilePlanCompoent(catOne, TYPE_RECORD_FOLDER); + + setComplete(); + endTransaction(); + + final String user = transactionHelper.doInTransaction(new RetryingTransactionCallback() + { + public String execute() throws Throwable + { + // Create a new role + Set caps = new HashSet(1); + caps.add(rmSecurityService.getCapability(RMPermissionModel.VIEW_RECORDS)); + + Role role = rmSecurityService.createRole(rmRootNode, "TestRole", "My Test Role", caps); + String user = createUser(); + + // Check the role group and allRole group are set up correctly + Set groups = authorityService.getContainingAuthorities(AuthorityType.GROUP, role.getRoleGroupName(), true); + assertNotNull(groups); + // expect allRole group and the capability group + assertEquals(1, groups.size()); + List tempList = new ArrayList(groups); + assertTrue(tempList.get(0).startsWith("GROUP_AllRoles")); + + // User shouldn't be able to see the file plan node + AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public Object doWork() throws Exception + { + // Check the permissions of the group on the root node + assertEquals(AccessStatus.DENIED, permissionService.hasPermission(rmRootNode, RMPermissionModel.READ_RECORDS)); + + try + { + nodeService.getChildAssocs(rmRootNode); + fail("The user shouldn't be able to read the children"); + } + catch (AlfrescoRuntimeException e) + { + // expected + } + + return null; + } + }, user); + + // Assign the new user to the role + rmSecurityService.assignRoleToAuthority(rmRootNode, role.getName(), user); + + return user; + } + }); + + transactionHelper.doInTransaction(new RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + // Prove that all the series are there + List assocs = nodeService.getChildAssocs(rmRootNode); + assertNotNull(assocs); + assertEquals(3, assocs.size()); + + // User should be able to see the file plan node + AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public Object doWork() throws Exception + { + // Check user has read on the root + // assertEquals(AccessStatus.ALLOWED, permissionService.hasPermission(rmRootNode, RMPermissionModel.READ_RECORDS)); + + // Check that the user can not see any of the series + List assocs = nodeService.getChildAssocs(rmRootNode); + assertNotNull(assocs); + assertEquals(0, assocs.size()); + + return null; + } + }, user); + + // Add read permissions to one of the series + permissionService.setPermission(seriesOne, user, RMPermissionModel.READ_RECORDS, true); + + // Show that user can now see that series + AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public Object doWork() throws Exception + { + // Check that the user can not see any of the series + List assocs = nodeService.getChildAssocs(rmRootNode); + assertNotNull(assocs); + assertEquals(1, assocs.size()); + + return null; + } + }, user); + + // Add the read permission and file permission to get to the folder + permissionService.setPermission(catOne, user, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(folderOne, user, RMPermissionModel.FILING, true); + + // TODO check visibility of items as we add the permissions + // TODO check that records inherit the permissions ok + + // Try and close the folder as the new user + AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public Object doWork() throws Exception + { + try + { + rmActionService.executeRecordsManagementAction(folderOne, "closeRecordFolder"); + fail("User does not have the capability for this"); + } + catch (org.alfresco.repo.security.permissions.AccessDeniedException exception) + { + // expected + } + + return null; + } + }, user); + + // Add the capability to the role + Set caps2 = new HashSet(1); + caps2.add(rmSecurityService.getCapability(RMPermissionModel.VIEW_RECORDS)); + caps2.add(rmSecurityService.getCapability(RMPermissionModel.CLOSE_FOLDERS)); + rmSecurityService.updateRole(rmRootNode, "TestRole", "My Test Role", caps2); + + Set aps = permissionService.getAllSetPermissions(rmRootNode); + System.out.println("\nPermissions on new series node: "); + for (AccessPermission ap : aps) + { + System.out.println(" - " + ap.getAuthority() + " has " + ap.getPermission()); + } + + // Try and close the folder as the new user + AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public Object doWork() throws Exception + { + assertEquals(AccessStatus.ALLOWED, permissionService.hasPermission(rmRootNode, RMPermissionModel.CLOSE_FOLDERS)); + + rmActionService.executeRecordsManagementAction(folderOne, "closeRecordFolder"); + + return null; + } + }, user); + + return null; + } + }); + } + + public void testSetPermissions() + { + final NodeRef rmRootNode = createRMRootNodeRef(); + + final NodeRef seriesOne = addFilePlanCompoent(rmRootNode, TYPE_RECORD_CATEGORY); + final NodeRef seriesTwo = addFilePlanCompoent(rmRootNode, TYPE_RECORD_CATEGORY); + final NodeRef seriesThree = addFilePlanCompoent(rmRootNode, TYPE_RECORD_CATEGORY); + + final NodeRef catOne = addFilePlanCompoent(seriesOne, TYPE_RECORD_CATEGORY); + final NodeRef catTwo = addFilePlanCompoent(seriesOne, TYPE_RECORD_CATEGORY); + final NodeRef catThree = addFilePlanCompoent(seriesOne, TYPE_RECORD_CATEGORY); + + final NodeRef folderOne = addFilePlanCompoent(catOne, TYPE_RECORD_FOLDER); + final NodeRef folderTwo = addFilePlanCompoent(catOne, TYPE_RECORD_FOLDER); + final NodeRef folderThree = addFilePlanCompoent(catOne, TYPE_RECORD_FOLDER); + + setComplete(); + endTransaction(); + + transactionHelper.doInTransaction(new RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + // Create a new role + Set caps = new HashSet(1); + caps.add(rmSecurityService.getCapability(RMPermissionModel.VIEW_RECORDS)); + + Role role = rmSecurityService.createRole(rmRootNode, "TestRole", "My Test Role", caps); + String user = createUser(); + + rmSecurityService.assignRoleToAuthority(rmRootNode, role.getName(), user); + + AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public Object doWork() throws Exception + { + assertEquals(AccessStatus.ALLOWED, permissionService.hasPermission(rmRootNode, RMPermissionModel.READ_RECORDS)); + assertEquals(AccessStatus.DENIED, permissionService.hasPermission(seriesOne, RMPermissionModel.READ_RECORDS)); + assertEquals(AccessStatus.DENIED, permissionService.hasPermission(seriesTwo, RMPermissionModel.READ_RECORDS)); + assertEquals(AccessStatus.DENIED, permissionService.hasPermission(seriesThree, RMPermissionModel.READ_RECORDS)); + assertEquals(AccessStatus.DENIED, permissionService.hasPermission(catOne, RMPermissionModel.READ_RECORDS)); + assertEquals(AccessStatus.DENIED, permissionService.hasPermission(catTwo, RMPermissionModel.READ_RECORDS)); + assertEquals(AccessStatus.DENIED, permissionService.hasPermission(catThree, RMPermissionModel.READ_RECORDS)); + assertEquals(AccessStatus.DENIED, permissionService.hasPermission(folderOne, RMPermissionModel.READ_RECORDS)); + assertEquals(AccessStatus.DENIED, permissionService.hasPermission(folderTwo, RMPermissionModel.READ_RECORDS)); + assertEquals(AccessStatus.DENIED, permissionService.hasPermission(folderThree, RMPermissionModel.READ_RECORDS)); + + assertEquals(AccessStatus.DENIED, permissionService.hasPermission(rmRootNode, RMPermissionModel.FILING)); + assertEquals(AccessStatus.DENIED, permissionService.hasPermission(seriesOne, RMPermissionModel.FILING)); + assertEquals(AccessStatus.DENIED, permissionService.hasPermission(seriesTwo, RMPermissionModel.FILING)); + assertEquals(AccessStatus.DENIED, permissionService.hasPermission(seriesThree, RMPermissionModel.FILING)); + assertEquals(AccessStatus.DENIED, permissionService.hasPermission(catOne, RMPermissionModel.FILING)); + assertEquals(AccessStatus.DENIED, permissionService.hasPermission(catTwo, RMPermissionModel.FILING)); + assertEquals(AccessStatus.DENIED, permissionService.hasPermission(catThree, RMPermissionModel.FILING)); + assertEquals(AccessStatus.DENIED, permissionService.hasPermission(folderOne, RMPermissionModel.FILING)); + assertEquals(AccessStatus.DENIED, permissionService.hasPermission(folderTwo, RMPermissionModel.FILING)); + assertEquals(AccessStatus.DENIED, permissionService.hasPermission(folderThree, RMPermissionModel.FILING)); + + return null; + } + }, user); + + rmSecurityService.setPermission(catOne, user, RMPermissionModel.FILING); + + AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + public Object doWork() throws Exception + { + assertEquals(AccessStatus.ALLOWED, permissionService.hasPermission(rmRootNode, RMPermissionModel.READ_RECORDS)); + assertEquals(AccessStatus.ALLOWED, permissionService.hasPermission(seriesOne, RMPermissionModel.READ_RECORDS)); + assertEquals(AccessStatus.DENIED, permissionService.hasPermission(seriesTwo, RMPermissionModel.READ_RECORDS)); + assertEquals(AccessStatus.DENIED, permissionService.hasPermission(seriesThree, RMPermissionModel.READ_RECORDS)); + assertEquals(AccessStatus.ALLOWED, permissionService.hasPermission(catOne, RMPermissionModel.READ_RECORDS)); + assertEquals(AccessStatus.DENIED, permissionService.hasPermission(catTwo, RMPermissionModel.READ_RECORDS)); + assertEquals(AccessStatus.DENIED, permissionService.hasPermission(catThree, RMPermissionModel.READ_RECORDS)); + assertEquals(AccessStatus.ALLOWED, permissionService.hasPermission(folderOne, RMPermissionModel.READ_RECORDS)); + assertEquals(AccessStatus.ALLOWED, permissionService.hasPermission(folderTwo, RMPermissionModel.READ_RECORDS)); + assertEquals(AccessStatus.ALLOWED, permissionService.hasPermission(folderThree, RMPermissionModel.READ_RECORDS)); + + assertEquals(AccessStatus.DENIED, permissionService.hasPermission(rmRootNode, RMPermissionModel.FILING)); + assertEquals(AccessStatus.DENIED, permissionService.hasPermission(seriesOne, RMPermissionModel.FILING)); + assertEquals(AccessStatus.DENIED, permissionService.hasPermission(seriesTwo, RMPermissionModel.FILING)); + assertEquals(AccessStatus.DENIED, permissionService.hasPermission(seriesThree, RMPermissionModel.FILING)); + assertEquals(AccessStatus.ALLOWED, permissionService.hasPermission(catOne, RMPermissionModel.FILING)); + assertEquals(AccessStatus.DENIED, permissionService.hasPermission(catTwo, RMPermissionModel.FILING)); + assertEquals(AccessStatus.DENIED, permissionService.hasPermission(catThree, RMPermissionModel.FILING)); + assertEquals(AccessStatus.ALLOWED, permissionService.hasPermission(folderOne, RMPermissionModel.FILING)); + assertEquals(AccessStatus.ALLOWED, permissionService.hasPermission(folderTwo, RMPermissionModel.FILING)); + assertEquals(AccessStatus.ALLOWED, permissionService.hasPermission(folderThree, RMPermissionModel.FILING)); + + return null; + } + }, user); + + return null; + } + }); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementServiceImplTest.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementServiceImplTest.java new file mode 100644 index 0000000000..3c7f77db0d --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementServiceImplTest.java @@ -0,0 +1,670 @@ +/* + * 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.module.org_alfresco_module_rm.test.service; + +import java.util.List; +import java.util.Set; + +import org.alfresco.module.org_alfresco_module_rm.FilePlanComponentKind; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.dod5015.DOD5015Model; +import org.alfresco.module.org_alfresco_module_rm.test.util.BaseRMTestCase; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.GUID; +import org.springframework.util.CollectionUtils; + + +/** + * Records management service test. + * + * @author Roy Wetherall + */ +public class RecordsManagementServiceImplTest extends BaseRMTestCase +{ + /********** RM Component methods **********/ + + /** + * @see RecordsManagementService#isFilePlanComponent(org.alfresco.service.cmr.repository.NodeRef) + */ + public void testIsFilePlanComponent() throws Exception + { + doTestInTransaction(new Test() + { + @Override + public NodeRef run() + { + assertTrue("The rm root container should be a rm component", rmService.isFilePlanComponent(filePlan)); + assertTrue("The rm container should be a rm component", rmService.isFilePlanComponent(rmContainer)); + assertTrue("The rm folder should be a rm component", rmService.isFilePlanComponent(rmFolder)); + + return null; + } + }); + } + + /** + * @see RecordsManagementService#getFilePlanComponentKind(NodeRef) + */ + public void testGetFilePlanComponentKind() throws Exception + { + doTestInTransaction(new Test() + { + @Override + public NodeRef run() throws Exception + { + return createRecord(rmFolder, "testRecord.txt"); + } + + @Override + public void test(NodeRef result) throws Exception + { + assertEquals(FilePlanComponentKind.FILE_PLAN, rmService.getFilePlanComponentKind(filePlan)); + assertEquals(FilePlanComponentKind.RECORD_CATEGORY, rmService.getFilePlanComponentKind(rmContainer)); + assertEquals(FilePlanComponentKind.RECORD_FOLDER, rmService.getFilePlanComponentKind(rmFolder)); + assertEquals(FilePlanComponentKind.RECORD, rmService.getFilePlanComponentKind(result)); + // TODO HOLD and TRANSFER + assertNull(rmService.getFilePlanComponentKind(folder)); + } + }); + } + + /** + * @see RecordsManagementService#isFilePlan(NodeRef) + */ + public void testIsFilePlan() throws Exception + { + doTestInTransaction(new Test() + { + @Override + public NodeRef run() + { + assertTrue("This is a records management root", rmService.isFilePlan(filePlan)); + assertFalse("This should not be a records management root", rmService.isFilePlan(rmContainer)); + assertFalse("This should not be a records management root", rmService.isFilePlan(rmFolder)); + + return null; + } + }); + } + + /** + * @see RecordsManagementService#isRecordCategory(NodeRef) + */ + public void testIsRecordCategory() throws Exception + { + doTestInTransaction(new Test() + { + @Override + public NodeRef run() + { + assertFalse("This should not be a record category.", rmService.isRecordCategory(filePlan)); + assertTrue("This is a record category.", rmService.isRecordCategory(rmContainer)); + assertFalse("This should not be a record category.", rmService.isRecordCategory(rmFolder)); + + return null; + } + }); + } + + /** + * @see RecordsManagementService#isRecordFolder(NodeRef) + */ + public void testIsRecordFolder() throws Exception + { + doTestInTransaction(new Test() + { + @Override + public NodeRef run() + { + assertFalse("This should not be a record folder", rmService.isRecordFolder(filePlan)); + assertFalse("This should not be a record folder", rmService.isRecordFolder(rmContainer)); + assertTrue("This should be a record folder", rmService.isRecordFolder(rmFolder)); + + return null; + } + }); + } + + // TODO void testIsRecord() + + // TODO void testIsHold() + + // TODO void testIsTransfer() + + // TODO void testGetNodeRefPath() + + /** + * @see RecordsManagementService#getRecordsManagementRoot() + */ + public void testGetRecordsManagementRoot() throws Exception + { + doTestInTransaction(new Test() + { + @Override + public NodeRef run() + { + assertEquals(filePlan, rmService.getFilePlan(filePlan)); + assertEquals(filePlan, rmService.getFilePlan(rmContainer)); + assertEquals(filePlan, rmService.getFilePlan(rmFolder)); + + return null; + } + }); + } + + /********** Record Management Root methods **********/ + + /** + * @see RecordsManagementService#getFilePlans() + */ + public void testGetRecordsManagementRoots() throws Exception + { + doTestInTransaction(new Test() + { + @Override + public NodeRef run() + { +// List roots = rmService.getRecordsManagementRoots(storeRef); +// assertNotNull(roots); +// assertTrue(roots.size() != 0); +// assertTrue(roots.contains(rmRootContainer)); +// +// RecordsManagementServiceImpl temp = (RecordsManagementServiceImpl)applicationContext.getBean("recordsManagementService"); +// temp.setDefaultStoreRef(storeRef); + + List roots = rmService.getFilePlans(); + assertNotNull(roots); + assertTrue(roots.size() != 0); + assertTrue(roots.contains(filePlan)); + + return null; + } + }); + } + + /** + * @see RecordsManagementService#createFilePlan(org.alfresco.service.cmr.repository.NodeRef, String) + * @see RecordsManagementService#createFilePlan(org.alfresco.service.cmr.repository.NodeRef, String, org.alfresco.service.namespace.QName) + */ + public void testCreateFilePlan() throws Exception + { + // Create default type of root + doTestInTransaction(new Test() + { + @Override + public NodeRef run() + { + String id = setString("id", GUID.generate()); + return rmService.createFilePlan(folder, id); + } + + @Override + public void test(NodeRef result) + { + assertNotNull("Unable to create records management root", result); + basicRMContainerCheck(result, getString("id"), TYPE_FILE_PLAN); + } + }); + + // Create specific type of root + doTestInTransaction(new Test() + { + @Override + public NodeRef run() + { + String id = setString("id", GUID.generate()); + return rmService.createFilePlan(folder, id, TYPE_FILE_PLAN); + } + + @Override + public void test(NodeRef result) + { + assertNotNull("Unable to create records management root", result); + basicRMContainerCheck(result, getString("id"), TYPE_FILE_PLAN); + } + }); + + // Failure: creating root in existing hierarchy + doTestInTransaction(new FailureTest() + { + @Override + public void run() + { + rmService.createFilePlan(rmContainer, GUID.generate()); + } + }); + + // Failure: type no extended from root container + doTestInTransaction(new FailureTest() + { + @Override + public void run() + { + rmService.createFilePlan(folder, GUID.generate(), TYPE_FOLDER); + } + }); + } + + /********** Records Management Container methods **********/ + + /** + * @see RecordsManagementService#createRecordCategory(NodeRef, String) + * @see RecordsManagementService#createRecordCategory(NodeRef, String, org.alfresco.service.namespace.QName) + */ + public void testCreateRecordCategory() throws Exception + { + // Create container (in root) + doTestInTransaction(new Test() + { + @Override + public NodeRef run() + { + String id = setString("id", GUID.generate()); + return rmService.createRecordCategory(filePlan, id); + } + + @Override + public void test(NodeRef result) + { + assertNotNull("Unable to create records management container", result); + basicRMContainerCheck(result, getString("id"), TYPE_RECORD_CATEGORY); + } + }); + + // Create container (in container) + doTestInTransaction(new Test() + { + @Override + public NodeRef run() + { + String id = setString("id", GUID.generate()); + return rmService.createRecordCategory(rmContainer, id); + } + + @Override + public void test(NodeRef result) + { + assertNotNull("Unable to create records management container", result); + basicRMContainerCheck(result, getString("id"), TYPE_RECORD_CATEGORY); + } + }); + + // TODO need a custom type of container! + // Create container of a given type +// doTestInTransaction(new Test() +// { +// @Override +// public NodeRef run() +// { +// String id = setString("id", GUID.generate()); +// return rmService.createRecordCategory(filePlan, id, TYPE_RECORD_SERIES); +// } +// +// @Override +// public void test(NodeRef result) +// { +// assertNotNull("Unable to create records management container", result); +// basicRMContainerCheck(result, getString("id"), TYPE_RECORD_SERIES); +// } +// }); + + // Fail Test: parent is not a container + doTestInTransaction(new FailureTest() + { + @Override + public void run() + { + rmService.createRecordCategory(folder, GUID.generate()); + } + }); + + // Fail Test: type is not a sub-type of rm:recordsManagementContainer + doTestInTransaction(new FailureTest() + { + @Override + public void run() + { + rmService.createRecordCategory(filePlan, GUID.generate(), TYPE_FOLDER); + } + }); + } + + /** + * @see RecordsManagementService#getAllContained(NodeRef) + * @see RecordsManagementService#getAllContained(NodeRef, boolean) + */ + public void testGetAllContained() throws Exception + { + // Get all contained test + doTestInTransaction(new Test() + { + @Override + public Void run() + { + // Add to the test data + NodeRef series = rmService.createRecordCategory(rmContainer, "rmSeries"); + NodeRef seriesChildFolder = rmService.createRecordFolder(series, "seriesRecordFolder"); + NodeRef seriesChildContainer = rmService.createRecordCategory(series, "childContainer"); + + // Put in model + setNodeRef("series", series); + setNodeRef("seriesChildFolder", seriesChildFolder); + setNodeRef("seriesChildContainer", seriesChildContainer); + + return null; + } + + @Override + public void test(Void result) throws Exception + { + List nodes = rmService.getAllContained(rmContainer); + assertNotNull(nodes); + assertEquals(2, nodes.size()); + assertTrue(nodes.contains(getNodeRef("series"))); + assertTrue(nodes.contains(rmFolder)); + + nodes = rmService.getAllContained(rmContainer, false); + assertNotNull(nodes); + assertEquals(2, nodes.size()); + assertTrue(nodes.contains(getNodeRef("series"))); + assertTrue(nodes.contains(rmFolder)); + + nodes = rmService.getAllContained(rmContainer, true); + assertNotNull(nodes); + assertEquals(4, nodes.size()); + assertTrue(nodes.contains(getNodeRef("series"))); + assertTrue(nodes.contains(rmFolder)); + assertTrue(nodes.contains(getNodeRef("seriesChildFolder"))); + assertTrue(nodes.contains(getNodeRef("seriesChildContainer"))); + + } + }); + + // Failure: call on record folder + doTestInTransaction(new FailureTest() + { + @Override + public void run() + { + rmService.getAllContained(rmFolder); + } + }); + } + + /** + * @see RecordsManagementService#getContainedRecordCategories(NodeRef) + * @see RecordsManagementService#getContainedRecordCategories(NodeRef, boolean) + */ + public void testGetContainedRecordCategories() throws Exception + { + // Test getting all contained containers + doTestInTransaction(new Test() + { + @Override + public Void run() + { + // Add to the test data + NodeRef series = rmService.createRecordCategory(rmContainer, "rmSeries"); + NodeRef seriesChildFolder = rmService.createRecordFolder(series, "seriesRecordFolder"); + NodeRef seriesChildContainer = rmService.createRecordCategory(series, "childContainer"); + + // Put in model + setNodeRef("series", series); + setNodeRef("seriesChildFolder", seriesChildFolder); + setNodeRef("seriesChildContainer", seriesChildContainer); + + return null; + } + + @Override + public void test(Void result) throws Exception + { + List nodes = rmService.getContainedRecordCategories(rmContainer); + assertNotNull(nodes); + assertEquals(1, nodes.size()); + assertTrue(nodes.contains(getNodeRef("series"))); + + nodes = rmService.getContainedRecordCategories(rmContainer, false); + assertNotNull(nodes); + assertEquals(1, nodes.size()); + assertTrue(nodes.contains(getNodeRef("series"))); + + nodes = rmService.getContainedRecordCategories(rmContainer, true); + assertNotNull(nodes); + assertEquals(2, nodes.size()); + assertTrue(nodes.contains(getNodeRef("series"))); + assertTrue(nodes.contains(getNodeRef("seriesChildContainer"))); + } + }); + + // Failure: call on record folder + doTestInTransaction(new FailureTest() + { + @Override + public void run() + { + rmService.getContainedRecordCategories(rmFolder); + } + }); + } + + /** + * @see RecordsManagementService#getContainedRecordFolders(NodeRef) + * @see RecordsManagementService#getContainedRecordFolders(NodeRef, boolean) + */ + public void testGetContainedRecordFolders() throws Exception + { + // Test getting all contained record folders + doTestInTransaction(new Test() + { + @Override + public Void run() + { + // Add to the test data + NodeRef series = rmService.createRecordCategory(rmContainer, "rmSeries"); + NodeRef seriesChildFolder = rmService.createRecordFolder(series, "seriesRecordFolder"); + NodeRef seriesChildContainer = rmService.createRecordCategory(series, "childContainer"); + + // Put in model + setNodeRef("series", series); + setNodeRef("seriesChildFolder", seriesChildFolder); + setNodeRef("seriesChildContainer", seriesChildContainer); + + return null; + } + + @Override + public void test(Void result) throws Exception + { + List nodes = rmService.getContainedRecordFolders(rmContainer); + assertNotNull(nodes); + assertEquals(1, nodes.size()); + assertTrue(nodes.contains(rmFolder)); + + nodes = rmService.getContainedRecordFolders(rmContainer, false); + assertNotNull(nodes); + assertEquals(1, nodes.size()); + assertTrue(nodes.contains(rmFolder)); + + nodes = rmService.getContainedRecordFolders(rmContainer, true); + assertNotNull(nodes); + assertEquals(2, nodes.size()); + assertTrue(nodes.contains(rmFolder)); + assertTrue(nodes.contains(getNodeRef("seriesChildFolder"))); + } + }); + + // Failure: call on record folder + doTestInTransaction(new FailureTest() + { + @Override + public void run() + { + rmService.getContainedRecordFolders(rmFolder); + } + }); + } + + /********** Record Folder methods **********/ + + // TODO void testIsRecordFolderDeclared() + + // TODO void testIsRecordFolderClosed() + + // TODO void testGetRecords() + + /** + * @see RecordsManagementService#createRecordFolder(NodeRef, String) + * @see RecordsManagementService#createRecordFolder(NodeRef, String, QName) + */ + public void testCreateRecordFolder() throws Exception + { + // Create record + doTestInTransaction(new Test() + { + @Override + public NodeRef run() + { + String id = setString("id", GUID.generate()); + return rmService.createRecordFolder(rmContainer, id); + } + + @Override + public void test(NodeRef result) + { + assertNotNull("Unable to create record folder", result); + basicRMContainerCheck(result, getString("id"), TYPE_RECORD_FOLDER); + } + }); + + // TODO Create record of type + + // Failure: Create record with invalid type + doTestInTransaction(new FailureTest() + { + @Override + public void run() + { + rmService.createRecordFolder(rmContainer, GUID.generate(), TYPE_FOLDER); + } + }); + + // Failure: Create record folder in root + doTestInTransaction(new FailureTest() + { + @Override + public void run() + { + rmService.createRecordFolder(filePlan, GUID.generate()); + } + }); + } + + /********** Record methods **********/ + + // TODO void testIsRecordFrozen() + + /** + * @see RecordsManagementService#getRecordMetaDataAspects() + */ + public void testGetRecordMetaDataAspects() + { + doTestInTransaction(new Test() + { + @Override + public Void run() + { + Set aspects = rmService.getRecordMetaDataAspects(); + assertNotNull(aspects); + assertEquals(5, aspects.size()); + assertTrue(aspects.containsAll( + CollectionUtils.arrayToList(new QName[] + { + DOD5015Model.ASPECT_DIGITAL_PHOTOGRAPH_RECORD, + DOD5015Model.ASPECT_PDF_RECORD, + DOD5015Model.ASPECT_WEB_RECORD, + DOD5015Model.ASPECT_SCANNED_RECORD, + ASPECT_RECORD_META_DATA + }))); + + return null; + } + }); + } + + // TODO void testGetRecordFolders(NodeRef record); + + // TODO void testIsRecordDeclared(NodeRef nodeRef); + + /********** RM2 - Multi-hierarchy record taxonomy's **********/ + + /** + * Test to create a simple multi-hierarchy record taxonomy + */ + public void testCreateSimpleHierarchy() + { + doTestInTransaction(new Test() + { + @Override + public Void run() + { + // Create 3 level hierarchy + NodeRef levelOne = setNodeRef("container1", rmService.createRecordCategory(filePlan, "container1")); + assertNotNull("Unable to create container", levelOne); + NodeRef levelTwo = setNodeRef("container2", rmService.createRecordCategory(levelOne, "container2")); + assertNotNull("Unable to create container", levelTwo); + NodeRef levelThree = setNodeRef("container3", rmService.createRecordCategory(levelTwo, "container3")); + assertNotNull("Unable to create container", levelThree); + NodeRef levelThreeRecordFolder = setNodeRef("recordFolder3", rmService.createRecordFolder(levelThree, "recordFolder3")); + assertNotNull("Unable to create record folder", levelThreeRecordFolder); + + return null; + } + + @Override + public void test(Void result) + { + // Test that the hierarchy has been created correctly + basicRMContainerCheck(getNodeRef("container1"), "container1", TYPE_RECORD_CATEGORY); + basicRMContainerCheck(getNodeRef("container2"), "container2", TYPE_RECORD_CATEGORY); + basicRMContainerCheck(getNodeRef("container3"), "container3", TYPE_RECORD_CATEGORY); + basicRMContainerCheck(getNodeRef("recordFolder3"), "recordFolder3", TYPE_RECORD_FOLDER); + + // TODO need to check that the parents and children can be retrieved correctly + } + }); + } + + /** + * A basic test of a records management container + * + * @param nodeRef node reference + * @param name name of the container + * @param type the type of container + */ + private void basicRMContainerCheck(NodeRef nodeRef, String name, QName type) + { + // Check the basic details + assertEquals(name, nodeService.getProperty(nodeRef, PROP_NAME)); + assertNotNull("RM id has not been set", nodeService.getProperty(nodeRef, PROP_IDENTIFIER)); + assertEquals(type, nodeService.getType(nodeRef)); + } + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/VitalRecordServiceImplTest.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/VitalRecordServiceImplTest.java new file mode 100644 index 0000000000..ed7ff8cdc8 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/VitalRecordServiceImplTest.java @@ -0,0 +1,381 @@ +/* + * 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.module.org_alfresco_module_rm.test.service; + +import java.util.Date; + +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementSearchBehaviour; +import org.alfresco.module.org_alfresco_module_rm.test.util.BaseRMTestCase; +import org.alfresco.module.org_alfresco_module_rm.vital.VitalRecordDefinition; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.Period; +import org.alfresco.util.GUID; + +/** + * Vital record service implementation unit test. + * + * @author Roy Wetherall + */ +public class VitalRecordServiceImplTest extends BaseRMTestCase +{ + /** Test periods */ + protected static final Period PERIOD_NONE = new Period("none|0"); + protected static final Period PERIOD_WEEK = new Period("week|1"); + protected static final Period PERIOD_MONTH = new Period("month|1"); + + /** Test records */ + private NodeRef mhRecord51; + private NodeRef mhRecord52; + private NodeRef mhRecord53; + private NodeRef mhRecord54; + private NodeRef mhRecord55; + + /** Indicate this is a multi hierarchy test */ + @Override + protected boolean isMultiHierarchyTest() + { + return true; + } + + /** vital record multi-hierarchy test data + * + * |--rmRootContainer (no vr def) + * | + * |--mhContainer (no vr def) + * | + * |--mhContainer-1-1 (has schedule - folder level) (no vr def) + * | | + * | |--mhContainer-2-1 (vr def) + * | | + * | |--mhContainer-3-1 (no vr def) + * | + * |--mhContainer-1-2 (has schedule - folder level) (no vr def) + * | + * |--mhContainer-2-2 (no vr def) + * | | + * | |--mhContainer-3-2 (vr def disabled) + * | | + * | |--mhContainer-3-3 (has schedule - record level) (vr def) + * | + * |--mhContainer-2-3 (has schedule - folder level) (vr def) + * | + * |--mhContainer-3-4 (no vr def) + * | + * |--mhContainer-3-5 (has schedule- record level) (vr def) + */ + @Override + protected void setupMultiHierarchyTestData() + { + // Load core test data + super.setupMultiHierarchyTestData(); + + // Setup vital record definitions + setupVitalRecordDefinition(mhContainer21, true, PERIOD_WEEK); + setupVitalRecordDefinition(mhContainer32, false, PERIOD_WEEK); + setupVitalRecordDefinition(mhContainer33, true, PERIOD_WEEK); + setupVitalRecordDefinition(mhContainer23, true, PERIOD_WEEK); + setupVitalRecordDefinition(mhContainer35, true, PERIOD_MONTH); + + // Create records + retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + mhRecord51 = createRecord(mhRecordFolder41, "record51.txt"); + mhRecord52 = createRecord(mhRecordFolder42, "record52.txt"); + mhRecord53 = createRecord(mhRecordFolder43, "record53.txt"); + mhRecord54 = createRecord(mhRecordFolder44, "record54.txt"); + mhRecord55 = createRecord(mhRecordFolder45, "record55.txt"); + + return null; + } + }); + } + + /** + * Helper to set up the vital record definition data in a transactional manner. + * + * @param nodeRef + * @param enabled + * @param period + */ + private void setupVitalRecordDefinition(final NodeRef nodeRef, final boolean enabled, final Period period) + { + retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + vitalRecordService.setVitalRecordDefintion(nodeRef, enabled, period); + return null; + } + }); + } + + /** + * Based on the initial data: + * - check category, folder and record raw values. + * - check search aspect values. + */ + public void testInit() + { + doTestInTransaction(new Test() + { + @Override + public Void run() + { + assertHasVitalRecordDefinition(mhContainer, false, null); + assertHasVitalRecordDefinition(mhContainer11, false, null); + assertHasVitalRecordDefinition(mhContainer12, false, null); + assertHasVitalRecordDefinition(mhContainer21, true, PERIOD_WEEK); + assertHasVitalRecordDefinition(mhContainer22, false, null); + assertHasVitalRecordDefinition(mhContainer23, true, PERIOD_WEEK); + assertHasVitalRecordDefinition(mhContainer31, true, PERIOD_WEEK); + assertHasVitalRecordDefinition(mhContainer32, false, PERIOD_WEEK); + assertHasVitalRecordDefinition(mhContainer33, true, PERIOD_WEEK); + assertHasVitalRecordDefinition(mhContainer34, true, PERIOD_WEEK); + assertHasVitalRecordDefinition(mhContainer35, true, PERIOD_MONTH); + + assertHasVitalRecordDefinition(mhRecordFolder41, true, PERIOD_WEEK); + assertHasVitalRecordDefinition(mhRecordFolder42, false, null); + assertHasVitalRecordDefinition(mhRecordFolder43, true, PERIOD_WEEK); + assertHasVitalRecordDefinition(mhRecordFolder44, true, PERIOD_WEEK); + assertHasVitalRecordDefinition(mhRecordFolder45, true, PERIOD_MONTH); + + assertVitalRecord(mhRecord51, true, PERIOD_WEEK); + assertVitalRecord(mhRecord52, false, null); + assertVitalRecord(mhRecord53, true, PERIOD_WEEK); + assertVitalRecord(mhRecord54, true, PERIOD_WEEK); + assertVitalRecord(mhRecord55, true, PERIOD_MONTH); + + return null; + } + }); + } + + /** + * Test that when new record categories and record folders are created in an existing file plan + * structure that they correctly inherit the correct vital record property values + */ + public void testValueInheritance() throws Exception + { + // Test record category value inheritance + doTestInTransaction(new Test() + { + @Override + public NodeRef run() + { + return rmService.createRecordCategory(mhContainer35, GUID.generate()); + } + + @Override + public void test(NodeRef result) throws Exception + { + assertHasVitalRecordDefinition(result, true, PERIOD_MONTH); + } + }); + + // Test record folder value inheritance + doTestInTransaction(new Test() + { + @Override + public NodeRef run() + { + return rmService.createRecordFolder(mhContainer32, GUID.generate()); + } + + @Override + public void test(NodeRef result) throws Exception + { + assertHasVitalRecordDefinition(result, false, PERIOD_WEEK); + } + }); + } + + /** + * Test to ensure that changes made to vital record definitions are reflected down the hierarchy. + */ + public void testChangesToVitalRecordDefinitions() throws Exception + { + // Override vital record definition + doTestInTransaction(new Test() + { + @Override + public Void run() + { + setupVitalRecordDefinition(mhContainer31, true, PERIOD_MONTH); + return null; + } + + @Override + public void test(Void result) throws Exception + { + assertHasVitalRecordDefinition(mhContainer, false, null); + assertHasVitalRecordDefinition(mhContainer11, false, null); + assertHasVitalRecordDefinition(mhContainer12, false, null); + assertHasVitalRecordDefinition(mhContainer21, true, PERIOD_WEEK); + assertHasVitalRecordDefinition(mhContainer22, false, null); + assertHasVitalRecordDefinition(mhContainer23, true, PERIOD_WEEK); + assertHasVitalRecordDefinition(mhContainer31, true, PERIOD_MONTH); + assertHasVitalRecordDefinition(mhContainer32, false, PERIOD_WEEK); + assertHasVitalRecordDefinition(mhContainer33, true, PERIOD_WEEK); + assertHasVitalRecordDefinition(mhContainer34, true, PERIOD_WEEK); + assertHasVitalRecordDefinition(mhContainer35, true, PERIOD_MONTH); + + assertHasVitalRecordDefinition(mhRecordFolder41, true, PERIOD_MONTH); + assertHasVitalRecordDefinition(mhRecordFolder42, false, null); + assertHasVitalRecordDefinition(mhRecordFolder43, true, PERIOD_WEEK); + assertHasVitalRecordDefinition(mhRecordFolder44, true, PERIOD_WEEK); + assertHasVitalRecordDefinition(mhRecordFolder45, true, PERIOD_MONTH); + + assertVitalRecord(mhRecord51, true, PERIOD_MONTH); + assertVitalRecord(mhRecord52, false, null); + assertVitalRecord(mhRecord53, true, PERIOD_WEEK); + assertVitalRecord(mhRecord54, true, PERIOD_WEEK); + assertVitalRecord(mhRecord55, true, PERIOD_MONTH); + } + }); + + // 'turn off' vital record def + doTestInTransaction(new Test() + { + @Override + public Void run() + { + setupVitalRecordDefinition(mhContainer31, false, PERIOD_NONE); + return null; + } + + @Override + public void test(Void result) throws Exception + { + assertHasVitalRecordDefinition(mhContainer, false, null); + assertHasVitalRecordDefinition(mhContainer11, false, null); + assertHasVitalRecordDefinition(mhContainer12, false, null); + assertHasVitalRecordDefinition(mhContainer21, true, PERIOD_WEEK); + assertHasVitalRecordDefinition(mhContainer22, false, null); + assertHasVitalRecordDefinition(mhContainer23, true, PERIOD_WEEK); + assertHasVitalRecordDefinition(mhContainer31, false, null); + assertHasVitalRecordDefinition(mhContainer32, false, PERIOD_WEEK); + assertHasVitalRecordDefinition(mhContainer33, true, PERIOD_WEEK); + assertHasVitalRecordDefinition(mhContainer34, true, PERIOD_WEEK); + assertHasVitalRecordDefinition(mhContainer35, true, PERIOD_MONTH); + + assertHasVitalRecordDefinition(mhRecordFolder41, false, null); + assertHasVitalRecordDefinition(mhRecordFolder42, false, null); + assertHasVitalRecordDefinition(mhRecordFolder43, true, PERIOD_WEEK); + assertHasVitalRecordDefinition(mhRecordFolder44, true, PERIOD_WEEK); + assertHasVitalRecordDefinition(mhRecordFolder45, true, PERIOD_MONTH); + + assertVitalRecord(mhRecord51, false, null); + assertVitalRecord(mhRecord52, false, null); + assertVitalRecord(mhRecord53, true, PERIOD_WEEK); + assertVitalRecord(mhRecord54, true, PERIOD_WEEK); + assertVitalRecord(mhRecord55, true, PERIOD_MONTH); + } + }); + + // Test parent change overrites existing + doTestInTransaction(new Test() + { + @Override + public Void run() + { + setupVitalRecordDefinition(mhContainer12, true, PERIOD_MONTH); + return null; + } + + @Override + public void test(Void result) throws Exception + { + assertHasVitalRecordDefinition(mhContainer, false, null); + assertHasVitalRecordDefinition(mhContainer11, false, null); + assertHasVitalRecordDefinition(mhContainer12, true, PERIOD_MONTH); + assertHasVitalRecordDefinition(mhContainer21, true, PERIOD_WEEK); + assertHasVitalRecordDefinition(mhContainer22, true, PERIOD_MONTH); + assertHasVitalRecordDefinition(mhContainer23, true, PERIOD_MONTH); + assertHasVitalRecordDefinition(mhContainer31, false, null); + assertHasVitalRecordDefinition(mhContainer32, true, PERIOD_MONTH); + assertHasVitalRecordDefinition(mhContainer33, true, PERIOD_MONTH); + assertHasVitalRecordDefinition(mhContainer34, true, PERIOD_MONTH); + assertHasVitalRecordDefinition(mhContainer35, true, PERIOD_MONTH); + + assertHasVitalRecordDefinition(mhRecordFolder41, false, null); + assertHasVitalRecordDefinition(mhRecordFolder42, true, PERIOD_MONTH); + assertHasVitalRecordDefinition(mhRecordFolder43, true, PERIOD_MONTH); + assertHasVitalRecordDefinition(mhRecordFolder44, true, PERIOD_MONTH); + assertHasVitalRecordDefinition(mhRecordFolder45, true, PERIOD_MONTH); + + assertVitalRecord(mhRecord51, false, null); + assertVitalRecord(mhRecord52, true, PERIOD_MONTH); + assertVitalRecord(mhRecord53, true, PERIOD_MONTH); + assertVitalRecord(mhRecord54, true, PERIOD_MONTH); + assertVitalRecord(mhRecord55, true, PERIOD_MONTH); + } + }); + + } + + @SuppressWarnings("deprecation") + private void assertHasVitalRecordDefinition(NodeRef nodeRef, boolean enabled, Period period) + { + assertTrue(nodeService.hasAspect(nodeRef, ASPECT_VITAL_RECORD_DEFINITION)); + + VitalRecordDefinition def = vitalRecordService.getVitalRecordDefinition(nodeRef); + assertNotNull(def); + + Boolean vitalRecordIndicator = (Boolean)nodeService.getProperty(nodeRef, PROP_VITAL_RECORD_INDICATOR); + assertNotNull(vitalRecordIndicator); + assertEquals(enabled, vitalRecordIndicator.booleanValue()); + assertEquals(enabled, def.isEnabled()); + + if (enabled == true) + { + Period reviewPeriod = (Period)nodeService.getProperty(nodeRef, PROP_REVIEW_PERIOD); + assertNotNull(reviewPeriod); + assertEquals(period, reviewPeriod); + assertEquals(period, def.getReviewPeriod()); + assertEquals(period.getNextDate(new Date()).getDate(), def.getNextReviewDate().getDate()); + } + } + + @SuppressWarnings("deprecation") + private void assertVitalRecord(NodeRef nodeRef, boolean enabled, Period period) + { + assertEquals(enabled, nodeService.hasAspect(nodeRef, ASPECT_VITAL_RECORD)); + if (enabled == true) + { + Date reviewAsOf = (Date)nodeService.getProperty(nodeRef, PROP_REVIEW_AS_OF); + assertNotNull(reviewAsOf); + assertEquals(period.getNextDate(new Date()).getDate(), reviewAsOf.getDate()); + + assertEquals(period.getPeriodType(), nodeService.getProperty(nodeRef, RecordsManagementSearchBehaviour.PROP_RS_VITAL_RECORD_REVIEW_PERIOD)); + assertEquals(period.getExpression(), nodeService.getProperty(nodeRef, RecordsManagementSearchBehaviour.PROP_RS_VITAL_RECORD_REVIEW_PERIOD_EXPRESSION)); + } + else + { + assertNull(nodeService.getProperty(nodeRef, RecordsManagementSearchBehaviour.PROP_RS_VITAL_RECORD_REVIEW_PERIOD)); + assertNull(nodeService.getProperty(nodeRef, RecordsManagementSearchBehaviour.PROP_RS_VITAL_RECORD_REVIEW_PERIOD_EXPRESSION)); + } + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/system/CapabilitiesSystemTest.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/system/CapabilitiesSystemTest.java new file mode 100644 index 0000000000..55d7ced40e --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/system/CapabilitiesSystemTest.java @@ -0,0 +1,8849 @@ +/* + * 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.module.org_alfresco_module_rm.test.system; + +import java.io.Serializable; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.transaction.Status; +import javax.transaction.UserTransaction; + +import junit.framework.TestCase; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService; +import org.alfresco.module.org_alfresco_module_rm.action.impl.CompleteEventAction; +import org.alfresco.module.org_alfresco_module_rm.action.impl.FreezeAction; +import org.alfresco.module.org_alfresco_module_rm.action.impl.TransferAction; +import org.alfresco.module.org_alfresco_module_rm.action.impl.TransferCompleteAction; +import org.alfresco.module.org_alfresco_module_rm.capability.Capability; +import org.alfresco.module.org_alfresco_module_rm.capability.CapabilityService; +import org.alfresco.module.org_alfresco_module_rm.capability.RMEntryVoter; +import org.alfresco.module.org_alfresco_module_rm.capability.RMPermissionModel; +import org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEventService; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.permissions.AccessDeniedException; +import org.alfresco.repo.security.permissions.PermissionReference; +import org.alfresco.repo.security.permissions.impl.model.PermissionModel; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +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; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.security.AccessPermission; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.AuthorityService; +import org.alfresco.service.cmr.security.AuthorityType; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.cmr.view.ImporterBinding; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ApplicationContextHelper; +import org.springframework.context.ApplicationContext; + +/** + * @author andyh + */ +public class CapabilitiesSystemTest extends TestCase implements RecordsManagementModel +{ + + private ApplicationContext ctx; + + private NodeRef rootNodeRef; + + private NodeService nodeService; + + private NodeService publicNodeService; + + private TransactionService transactionService; + + private UserTransaction testTX; + + private NodeRef filePlan; + + private PermissionService permissionService; + + private RecordsManagementService recordsManagementService; + + private RecordsManagementSecurityService recordsManagementSecurityService; + + private RecordsManagementActionService recordsManagementActionService; + + private RecordsManagementEventService recordsManagementEventService; + + private CapabilityService capabilityService; + + private PermissionModel permissionModel; + + private ContentService contentService; + + private NodeRef recordSeries; + + private NodeRef recordCategory_1; + + private NodeRef recordCategory_2; + + private NodeRef recordFolder_1; + + private NodeRef recordFolder_2; + + private NodeRef record_1; + + private NodeRef record_2; + + private RMEntryVoter rmEntryVoter; + + private AuthorityService authorityService; + + private String rmUsers; + + private String rmPowerUsers; + + private String rmSecurityOfficers; + + private String rmRecordsManagers; + + private String rmAdministrators; + + private PersonService personService; + + private String rm_user; + + private String rm_power_user; + + private String rm_security_officer; + + private String rm_records_manager; + + private String rm_administrator; + + private String test_user; + + private String testers; + + private NodeRef recordCategory_3; + + private NodeRef recordFolder_3; + + private NodeRef record_3; + + private ContentService publicContentService; + + /** + * @param name + */ + public CapabilitiesSystemTest(String name) + { + super(name); + } + + /* + * (non-Javadoc) + * + * @see junit.framework.TestCase#setUp() + */ + protected void setUp() throws Exception + { + ctx = ApplicationContextHelper.getApplicationContext(); + + super.setUp(); + + nodeService = (NodeService) ctx.getBean("dbNodeService"); + publicNodeService = (NodeService) ctx.getBean("NodeService"); + transactionService = (TransactionService) ctx.getBean("transactionComponent"); + permissionService = (PermissionService) ctx.getBean("permissionService"); + permissionModel = (PermissionModel) ctx.getBean("permissionsModelDAO"); + contentService = (ContentService) ctx.getBean("contentService"); + publicContentService = (ContentService) ctx.getBean("ContentService"); + authorityService = (AuthorityService) ctx.getBean("authorityService"); + personService = (PersonService) ctx.getBean("personService"); + capabilityService = (CapabilityService) ctx.getBean("CapabilityService"); + + recordsManagementService = (RecordsManagementService) ctx.getBean("RecordsManagementService"); + recordsManagementSecurityService = (RecordsManagementSecurityService) ctx.getBean("RecordsManagementSecurityService"); + recordsManagementActionService = (RecordsManagementActionService) ctx.getBean("RecordsManagementActionService"); + recordsManagementEventService = (RecordsManagementEventService) ctx.getBean("RecordsManagementEventService"); + rmEntryVoter = (RMEntryVoter) ctx.getBean("rmEntryVoter"); + + testTX = transactionService.getUserTransaction(); + testTX.begin(); + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + + StoreRef storeRef = nodeService.createStore(StoreRef.PROTOCOL_WORKSPACE, "Test_" + System.currentTimeMillis()); + rootNodeRef = nodeService.getRootNode(storeRef); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + + recordsManagementEventService.getEvents(); + recordsManagementEventService.addEvent("rmEventType.simple", "event", "My Event"); + + filePlan = nodeService.createNode(rootNodeRef, ContentModel.ASSOC_CHILDREN, TYPE_FILE_PLAN, TYPE_FILE_PLAN).getChildRef(); + recordSeries = createRecordSeries(filePlan, "RS", "RS-1", "Record Series", "My record series"); + recordCategory_1 = createRecordCategory(recordSeries, "Docs", "101-1", "Docs", "Docs", "week|1", true, false); + recordCategory_2 = createRecordCategory(recordSeries, "More Docs", "101-2", "More Docs", "More Docs", "week|1", true, true); + recordCategory_3 = createRecordCategory(recordSeries, "No disp schedule", "101-3", "No disp schedule", "No disp schedule", "week|1", true, null); + + testTX.commit(); + testTX = transactionService.getUserTransaction(); + testTX.begin(); + + recordFolder_1 = createRecordFolder(recordCategory_1, "F1", "101-3", "title", "description", "week|1", true); + recordFolder_2 = createRecordFolder(recordCategory_2, "F2", "102-3", "title", "description", "week|1", true); + recordFolder_3 = createRecordFolder(recordCategory_3, "F3", "103-3", "title", "description", "week|1", true); + record_1 = createRecord(recordFolder_1); + record_2 = createRecord(recordFolder_2); + record_3 = createRecord(recordFolder_3); + + // create people ... + + rm_user = "rm_user_" + storeRef.getIdentifier(); + rm_power_user = "rm_power_user_" + storeRef.getIdentifier(); + rm_security_officer = "rm_security_officer_" + storeRef.getIdentifier(); + rm_records_manager = "rm_records_manager_" + storeRef.getIdentifier(); + rm_administrator = "rm_administrator_" + storeRef.getIdentifier(); + + test_user = "test_user_" + storeRef.getIdentifier(); + + personService.createPerson(createDefaultProperties(rm_user)); + personService.createPerson(createDefaultProperties(rm_power_user)); + personService.createPerson(createDefaultProperties(rm_security_officer)); + personService.createPerson(createDefaultProperties(rm_records_manager)); + personService.createPerson(createDefaultProperties(rm_administrator)); + personService.createPerson(createDefaultProperties(test_user)); + + // create roles as groups + + rmUsers = authorityService.createAuthority(AuthorityType.GROUP, "RM_USER_" + storeRef.getIdentifier()); + rmPowerUsers = authorityService.createAuthority(AuthorityType.GROUP, "RM_POWER_USER_" + storeRef.getIdentifier()); + rmSecurityOfficers = authorityService.createAuthority(AuthorityType.GROUP, "RM_SECURITY_OFFICER_" + storeRef.getIdentifier()); + rmRecordsManagers = authorityService.createAuthority(AuthorityType.GROUP, "RM_RECORDS_MANAGER_" + storeRef.getIdentifier()); + rmAdministrators = authorityService.createAuthority(AuthorityType.GROUP, "RM_ADMINISTRATOR_" + storeRef.getIdentifier()); + testers = authorityService.createAuthority(AuthorityType.GROUP, "RM_TESTOR_" + storeRef.getIdentifier()); + + authorityService.addAuthority(testers, test_user); + + for (PermissionReference pr : permissionModel.getImmediateGranteePermissions(permissionModel.getPermissionReference(null, RMPermissionModel.ROLE_USER))) + { + setPermission(filePlan, rmUsers, pr.getName(), true); + } + authorityService.addAuthority(rmUsers, rm_user); + setPermission(filePlan, rm_user, RMPermissionModel.FILING, true); + + for (PermissionReference pr : permissionModel.getImmediateGranteePermissions(permissionModel.getPermissionReference(null, RMPermissionModel.ROLE_POWER_USER))) + { + setPermission(filePlan, rmPowerUsers, pr.getName(), true); + } + authorityService.addAuthority(rmPowerUsers, rm_power_user); + setPermission(filePlan, rm_power_user, RMPermissionModel.FILING, true); + + for (PermissionReference pr : permissionModel.getImmediateGranteePermissions(permissionModel.getPermissionReference(null, RMPermissionModel.ROLE_SECURITY_OFFICER))) + { + setPermission(filePlan, rmSecurityOfficers, pr.getName(), true); + } + authorityService.addAuthority(rmSecurityOfficers, rm_security_officer); + setPermission(filePlan, rm_security_officer, RMPermissionModel.FILING, true); + + for (PermissionReference pr : permissionModel.getImmediateGranteePermissions(permissionModel.getPermissionReference(null, RMPermissionModel.ROLE_RECORDS_MANAGER))) + { + setPermission(filePlan, rmRecordsManagers, pr.getName(), true); + } + authorityService.addAuthority(rmRecordsManagers, rm_records_manager); + setPermission(filePlan, rm_records_manager, RMPermissionModel.FILING, true); + + for (PermissionReference pr : permissionModel.getImmediateGranteePermissions(permissionModel.getPermissionReference(null, RMPermissionModel.ROLE_ADMINISTRATOR))) + { + setPermission(filePlan, rmAdministrators, pr.getName(), true); + } + authorityService.addAuthority(rmAdministrators, rm_administrator); + setPermission(filePlan, rm_administrator, RMPermissionModel.FILING, true); + + testTX.commit(); + testTX = transactionService.getUserTransaction(); + testTX.begin(); + } + + private void setPermission(NodeRef nodeRef, String authority, String permission, boolean allow) + { + permissionService.setPermission(nodeRef, authority, permission, allow); + if (permission.equals(RMPermissionModel.FILING)) + { + if (recordsManagementService.isRecordCategory(nodeRef) == true) + { + List assocs = nodeService.getChildAssocs(nodeRef, ContentModel.ASSOC_CONTAINS, RegexQNamePattern.MATCH_ALL); + for (ChildAssociationRef assoc : assocs) + { + NodeRef child = assoc.getChildRef(); + if (recordsManagementService.isRecordFolder(child) == true || recordsManagementService.isRecordCategory(child) == true) + { + setPermission(child, authority, permission, allow); + } + } + } + } + } + + private Map createDefaultProperties(String userName) + { + HashMap properties = new HashMap(); + properties.put(ContentModel.PROP_USERNAME, userName); + properties.put(ContentModel.PROP_HOMEFOLDER, null); + properties.put(ContentModel.PROP_FIRSTNAME, userName); + properties.put(ContentModel.PROP_LASTNAME, userName); + properties.put(ContentModel.PROP_EMAIL, userName); + properties.put(ContentModel.PROP_ORGID, ""); + return properties; + } + + private NodeRef createRecord(NodeRef recordFolder) + { + Map props = new HashMap(1); + props.put(ContentModel.PROP_NAME, "MyRecord.txt"); + NodeRef recordOne = this.nodeService.createNode(recordFolder, ContentModel.ASSOC_CONTAINS, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "MyRecord.txt"), + ContentModel.TYPE_CONTENT, props).getChildRef(); + + // Set the content + ContentWriter writer = this.contentService.getWriter(recordOne, ContentModel.PROP_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.setEncoding("UTF-8"); + writer.putContent("There is some content in this record"); + return recordOne; + } + + private NodeRef createRecordSeries(NodeRef filePlan, String name, String identifier, String title, String description) + { + HashMap properties = new HashMap(); + properties.put(ContentModel.PROP_NAME, name); + properties.put(PROP_IDENTIFIER, identifier); + properties.put(ContentModel.PROP_TITLE, title); + properties.put(ContentModel.PROP_DESCRIPTION, description); + NodeRef answer = nodeService.createNode(filePlan, ContentModel.ASSOC_CONTAINS, TYPE_RECORD_CATEGORY, TYPE_RECORD_CATEGORY, properties).getChildRef(); + permissionService.setInheritParentPermissions(answer, false); + return answer; + } + + private NodeRef createRecordCategory(NodeRef recordSeries, String name, String identifier, String title, String description, String review, boolean vital, + Boolean recordLevelDisposition) + { + HashMap properties = new HashMap(); + properties.put(ContentModel.PROP_NAME, name); + properties.put(PROP_IDENTIFIER, identifier); + properties.put(ContentModel.PROP_TITLE, title); + properties.put(ContentModel.PROP_DESCRIPTION, description); + properties.put(PROP_REVIEW_PERIOD, review); + properties.put(PROP_VITAL_RECORD_INDICATOR, vital); + NodeRef answer = nodeService.createNode(recordSeries, ContentModel.ASSOC_CONTAINS, TYPE_RECORD_CATEGORY, TYPE_RECORD_CATEGORY, properties) + .getChildRef(); + + if (recordLevelDisposition != null) + { + properties = new HashMap(); + properties.put(PROP_DISPOSITION_AUTHORITY, "N1-218-00-4 item 023"); + properties.put(PROP_DISPOSITION_INSTRUCTIONS, "Cut off monthly, hold 1 month, then destroy."); + properties.put(PROP_RECORD_LEVEL_DISPOSITION, recordLevelDisposition); + NodeRef ds = nodeService.createNode(answer, ASSOC_DISPOSITION_SCHEDULE, TYPE_DISPOSITION_SCHEDULE, TYPE_DISPOSITION_SCHEDULE, + properties).getChildRef(); + + createDispoistionAction(ds, "cutoff", "monthend|1", null, "event"); + createDispoistionAction(ds, "transfer", "month|1", null, null); + createDispoistionAction(ds, "accession", "month|1", null, null); + createDispoistionAction(ds, "destroy", "month|1", "{http://www.alfresco.org/model/recordsmanagement/1.0}cutOffDate", null); + } + permissionService.setInheritParentPermissions(answer, false); + return answer; + } + + private NodeRef createDispoistionAction(NodeRef disposition, String actionName, String period, String periodProperty, String event) + { + HashMap properties = new HashMap(); + properties.put(PROP_DISPOSITION_ACTION_NAME, actionName); + properties.put(PROP_DISPOSITION_PERIOD, period); + if (periodProperty != null) + { + properties.put(PROP_DISPOSITION_PERIOD_PROPERTY, periodProperty); + } + if (event != null) + { + properties.put(PROP_DISPOSITION_EVENT, event); + } + NodeRef answer = nodeService.createNode(disposition, ASSOC_DISPOSITION_ACTION_DEFINITIONS, TYPE_DISPOSITION_ACTION_DEFINITION, + TYPE_DISPOSITION_ACTION_DEFINITION, properties).getChildRef(); + return answer; + } + + private NodeRef createRecordFolder(NodeRef recordCategory, String name, String identifier, String title, String description, String review, boolean vital) + { + HashMap properties = new HashMap(); + properties.put(ContentModel.PROP_NAME, name); + properties.put(PROP_IDENTIFIER, identifier); + properties.put(ContentModel.PROP_TITLE, title); + properties.put(ContentModel.PROP_DESCRIPTION, description); + properties.put(PROP_REVIEW_PERIOD, review); + properties.put(PROP_VITAL_RECORD_INDICATOR, vital); + NodeRef answer = nodeService.createNode(recordCategory, ContentModel.ASSOC_CONTAINS, TYPE_RECORD_FOLDER, TYPE_RECORD_FOLDER, properties) + .getChildRef(); + permissionService.setInheritParentPermissions(answer, false); + return answer; + } + + /* + * (non-Javadoc) + * + * @see junit.framework.TestCase#tearDown() + */ + protected void tearDown() throws Exception + { + if (testTX.getStatus() == Status.STATUS_ACTIVE) + { + testTX.rollback(); + } + else if (testTX.getStatus() == Status.STATUS_MARKED_ROLLBACK) + { + testTX.rollback(); + } + AuthenticationUtil.clearCurrentSecurityContext(); + super.tearDown(); + } + + public void testPermissionsModel() + { + Set exposed = permissionModel.getExposedPermissions(RecordsManagementModel.ASPECT_FILE_PLAN_COMPONENT); + assertEquals(6, exposed.size()); + assertTrue(exposed.contains(permissionModel.getPermissionReference(RecordsManagementModel.ASPECT_FILE_PLAN_COMPONENT, RMPermissionModel.ROLE_ADMINISTRATOR))); + + Set all = permissionModel.getAllPermissions(RecordsManagementModel.ASPECT_FILE_PLAN_COMPONENT); + assertEquals(58 /* capbilities */* 2 + 5 /* roles */+ (2 /* Read+File */* 2) + 1 /* Filing */, all.size()); + + checkGranting(RMPermissionModel.ACCESS_AUDIT, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER); + checkGranting(RMPermissionModel.ADD_MODIFY_EVENT_DATES, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER, + RMPermissionModel.ROLE_SECURITY_OFFICER, RMPermissionModel.ROLE_POWER_USER); + checkGranting(RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER); + checkGranting(RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER); + checkGranting(RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER); + checkGranting(RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER); + checkGranting(RMPermissionModel.CHANGE_OR_DELETE_REFERENCES, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER); + checkGranting(RMPermissionModel.CLOSE_FOLDERS, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER, RMPermissionModel.ROLE_SECURITY_OFFICER, + RMPermissionModel.ROLE_POWER_USER); + checkGranting(RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER); + checkGranting(RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER, + RMPermissionModel.ROLE_SECURITY_OFFICER); + checkGranting(RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER); + checkGranting(RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER); + checkGranting(RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER); + checkGranting(RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER, + RMPermissionModel.ROLE_SECURITY_OFFICER, RMPermissionModel.ROLE_POWER_USER); + checkGranting(RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER); + checkGranting(RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER); + checkGranting(RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER); + checkGranting(RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER); + checkGranting(RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER); + checkGranting(RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER); + checkGranting(RMPermissionModel.CYCLE_VITAL_RECORDS, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER, RMPermissionModel.ROLE_SECURITY_OFFICER, + RMPermissionModel.ROLE_POWER_USER); + checkGranting(RMPermissionModel.DECLARE_AUDIT_AS_RECORD, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER); + checkGranting(RMPermissionModel.DECLARE_RECORDS, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER, RMPermissionModel.ROLE_SECURITY_OFFICER, + RMPermissionModel.ROLE_POWER_USER, RMPermissionModel.ROLE_USER); + checkGranting(RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER, + RMPermissionModel.ROLE_SECURITY_OFFICER, RMPermissionModel.ROLE_POWER_USER); + checkGranting(RMPermissionModel.DELETE_AUDIT, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER); + checkGranting(RMPermissionModel.DELETE_LINKS, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER); + checkGranting(RMPermissionModel.DELETE_RECORDS, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER); + checkGranting(RMPermissionModel.DESTROY_RECORDS, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER); + checkGranting(RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER); + checkGranting(RMPermissionModel.DISPLAY_RIGHTS_REPORT, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER); + checkGranting(RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER); + checkGranting(RMPermissionModel.EDIT_NON_RECORD_METADATA, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER, + RMPermissionModel.ROLE_SECURITY_OFFICER, RMPermissionModel.ROLE_POWER_USER); + checkGranting(RMPermissionModel.EDIT_RECORD_METADATA, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER, + RMPermissionModel.ROLE_SECURITY_OFFICER, RMPermissionModel.ROLE_POWER_USER); + checkGranting(RMPermissionModel.EDIT_SELECTION_LISTS, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER); + checkGranting(RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER); + checkGranting(RMPermissionModel.EXPORT_AUDIT, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER); + checkGranting(RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER); + // File does not exists + // checkGranting(RMPermissionModel.FILE_RECORDS, RMPermissionModel.ROLE_ADMINISTRATOR, + // RMPermissionModel.ROLE_RECORDS_MANAGER); + checkGranting(RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER); + checkGranting(RMPermissionModel.MANAGE_ACCESS_CONTROLS, RMPermissionModel.ROLE_ADMINISTRATOR); + checkGranting(RMPermissionModel.MANAGE_ACCESS_RIGHTS, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER); + checkGranting(RMPermissionModel.MANUALLY_CHANGE_DISPOSITION_DATES, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER); + checkGranting(RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER); + checkGranting(RMPermissionModel.MAP_EMAIL_METADATA, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER); + checkGranting(RMPermissionModel.MOVE_RECORDS, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER); + checkGranting(RMPermissionModel.PASSWORD_CONTROL, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER); + checkGranting(RMPermissionModel.PLANNING_REVIEW_CYCLES, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER, + RMPermissionModel.ROLE_SECURITY_OFFICER, RMPermissionModel.ROLE_POWER_USER); + checkGranting(RMPermissionModel.RE_OPEN_FOLDERS, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER, RMPermissionModel.ROLE_SECURITY_OFFICER, + RMPermissionModel.ROLE_POWER_USER); + checkGranting(RMPermissionModel.SELECT_AUDIT_METADATA, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER); + checkGranting(RMPermissionModel.TRIGGER_AN_EVENT, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER); + checkGranting(RMPermissionModel.UNDECLARE_RECORDS, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER); + checkGranting(RMPermissionModel.UNFREEZE, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER); + checkGranting(RMPermissionModel.UPDATE_CLASSIFICATION_DATES, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER, + RMPermissionModel.ROLE_SECURITY_OFFICER); + checkGranting(RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER, + RMPermissionModel.ROLE_SECURITY_OFFICER); + checkGranting(RMPermissionModel.UPDATE_TRIGGER_DATES, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER); + checkGranting(RMPermissionModel.UPDATE_VITAL_RECORD_CYCLE_INFORMATION, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER); + checkGranting(RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER, + RMPermissionModel.ROLE_SECURITY_OFFICER); + checkGranting(RMPermissionModel.VIEW_RECORDS, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER, RMPermissionModel.ROLE_SECURITY_OFFICER, + RMPermissionModel.ROLE_POWER_USER, RMPermissionModel.ROLE_USER); + checkGranting(RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, RMPermissionModel.ROLE_ADMINISTRATOR, RMPermissionModel.ROLE_RECORDS_MANAGER); + + } + + private void checkGranting(String permission, String... roles) + { + Set granting = permissionModel.getGrantingPermissions(permissionModel.getPermissionReference(RecordsManagementModel.ASPECT_FILE_PLAN_COMPONENT, + permission)); + Set test = new HashSet(); + test.addAll(granting); + Set nonRM = new HashSet(); + for (PermissionReference pr : granting) + { + if (!pr.getQName().equals(RecordsManagementModel.ASPECT_FILE_PLAN_COMPONENT)) + { + nonRM.add(pr); + } + } + test.removeAll(nonRM); + assertEquals(roles.length + 1, test.size()); + for (String role : roles) + { + assertTrue(test.contains(permissionModel.getPermissionReference(RecordsManagementModel.ASPECT_FILE_PLAN_COMPONENT, role))); + } + + } + + public void testConfig() + { + assertEquals(6, recordsManagementSecurityService.getProtectedAspects().size()); + assertEquals(13, recordsManagementSecurityService.getProtectedProperties().size()); + + // Test action wire up + assertEquals(0, recordsManagementSecurityService.getCapability(RMPermissionModel.ACCESS_AUDIT).getActionNames().size()); + assertEquals(2, recordsManagementSecurityService.getCapability(RMPermissionModel.ADD_MODIFY_EVENT_DATES).getActionNames().size()); + assertEquals(2, recordsManagementSecurityService.getCapability(RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF).getActionNames().size()); + assertEquals(0, recordsManagementSecurityService.getCapability(RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES).getActionNames().size()); + assertEquals(2, recordsManagementSecurityService.getCapability(RMPermissionModel.AUTHORIZE_ALL_TRANSFERS).getActionNames().size()); + assertEquals(2, recordsManagementSecurityService.getCapability(RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS).getActionNames().size()); + assertEquals(0, recordsManagementSecurityService.getCapability(RMPermissionModel.CHANGE_OR_DELETE_REFERENCES).getActionNames().size()); + assertEquals(1, recordsManagementSecurityService.getCapability(RMPermissionModel.CLOSE_FOLDERS).getActionNames().size()); + assertEquals(0, recordsManagementSecurityService.getCapability(RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS).getActionNames().size()); + assertEquals(0, recordsManagementSecurityService.getCapability(RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES).getActionNames().size()); + assertEquals(0, recordsManagementSecurityService.getCapability(RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS).getActionNames().size()); + assertEquals(0, recordsManagementSecurityService.getCapability(RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA).getActionNames().size()); + assertEquals(0, recordsManagementSecurityService.getCapability(RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES).getActionNames().size()); + assertEquals(0, recordsManagementSecurityService.getCapability(RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS).getActionNames().size()); + assertEquals(0, recordsManagementSecurityService.getCapability(RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES).getActionNames().size()); + assertEquals(0, recordsManagementSecurityService.getCapability(RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES).getActionNames().size()); + assertEquals(0, recordsManagementSecurityService.getCapability(RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES).getActionNames().size()); + assertEquals(0, recordsManagementSecurityService.getCapability(RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES).getActionNames().size()); + assertEquals(0, recordsManagementSecurityService.getCapability(RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS).getActionNames().size()); + assertEquals(0, recordsManagementSecurityService.getCapability(RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS).getActionNames().size()); + assertEquals(1, recordsManagementSecurityService.getCapability(RMPermissionModel.CYCLE_VITAL_RECORDS).getActionNames().size()); + assertEquals(0, recordsManagementSecurityService.getCapability(RMPermissionModel.DECLARE_AUDIT_AS_RECORD).getActionNames().size()); + assertEquals(2, recordsManagementSecurityService.getCapability(RMPermissionModel.DECLARE_RECORDS).getActionNames().size()); + assertEquals(1, recordsManagementSecurityService.getCapability(RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS).getActionNames().size()); + assertEquals(0, recordsManagementSecurityService.getCapability(RMPermissionModel.DELETE_AUDIT).getActionNames().size()); + assertEquals(0, recordsManagementSecurityService.getCapability(RMPermissionModel.DELETE_LINKS).getActionNames().size()); + assertEquals(0, recordsManagementSecurityService.getCapability(RMPermissionModel.DELETE_RECORDS).getActionNames().size()); + assertEquals(0, recordsManagementSecurityService.getCapability(RMPermissionModel.DESTROY_RECORDS).getActionNames().size()); + assertEquals(1, recordsManagementSecurityService.getCapability(RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION).getActionNames().size()); + assertEquals(0, recordsManagementSecurityService.getCapability(RMPermissionModel.DISPLAY_RIGHTS_REPORT).getActionNames().size()); + assertEquals(0, recordsManagementSecurityService.getCapability(RMPermissionModel.EDIT_DECLARED_RECORD_METADATA).getActionNames().size()); + assertEquals(0, recordsManagementSecurityService.getCapability(RMPermissionModel.EDIT_NON_RECORD_METADATA).getActionNames().size()); + assertEquals(0, recordsManagementSecurityService.getCapability(RMPermissionModel.EDIT_RECORD_METADATA).getActionNames().size()); + assertEquals(0, recordsManagementSecurityService.getCapability(RMPermissionModel.EDIT_SELECTION_LISTS).getActionNames().size()); + assertEquals(0, recordsManagementSecurityService.getCapability(RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES).getActionNames().size()); + assertEquals(0, recordsManagementSecurityService.getCapability(RMPermissionModel.EXPORT_AUDIT).getActionNames().size()); + assertEquals(1, recordsManagementSecurityService.getCapability(RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE).getActionNames().size()); + assertEquals(1, recordsManagementSecurityService.getCapability(RMPermissionModel.FILE_RECORDS).getActionNames().size()); + assertEquals(0, recordsManagementSecurityService.getCapability(RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY).getActionNames().size()); + assertEquals(0, recordsManagementSecurityService.getCapability(RMPermissionModel.MANAGE_ACCESS_CONTROLS).getActionNames().size()); + assertEquals(0, recordsManagementSecurityService.getCapability(RMPermissionModel.MANAGE_ACCESS_RIGHTS).getActionNames().size()); + assertEquals(1, recordsManagementSecurityService.getCapability(RMPermissionModel.MANUALLY_CHANGE_DISPOSITION_DATES).getActionNames().size()); + assertEquals(0, recordsManagementSecurityService.getCapability(RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA).getActionNames().size()); + assertEquals(0, recordsManagementSecurityService.getCapability(RMPermissionModel.MAP_EMAIL_METADATA).getActionNames().size()); + assertEquals(0, recordsManagementSecurityService.getCapability(RMPermissionModel.MOVE_RECORDS).getActionNames().size()); + assertEquals(0, recordsManagementSecurityService.getCapability(RMPermissionModel.PASSWORD_CONTROL).getActionNames().size()); + assertEquals(1, recordsManagementSecurityService.getCapability(RMPermissionModel.PLANNING_REVIEW_CYCLES).getActionNames().size()); + assertEquals(1, recordsManagementSecurityService.getCapability(RMPermissionModel.RE_OPEN_FOLDERS).getActionNames().size()); + assertEquals(0, recordsManagementSecurityService.getCapability(RMPermissionModel.SELECT_AUDIT_METADATA).getActionNames().size()); + assertEquals(0, recordsManagementSecurityService.getCapability(RMPermissionModel.TRIGGER_AN_EVENT).getActionNames().size()); + assertEquals(1, recordsManagementSecurityService.getCapability(RMPermissionModel.UNDECLARE_RECORDS).getActionNames().size()); + assertEquals(2, recordsManagementSecurityService.getCapability(RMPermissionModel.UNFREEZE).getActionNames().size()); + assertEquals(0, recordsManagementSecurityService.getCapability(RMPermissionModel.UPDATE_CLASSIFICATION_DATES).getActionNames().size()); + assertEquals(0, recordsManagementSecurityService.getCapability(RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES).getActionNames().size()); + assertEquals(0, recordsManagementSecurityService.getCapability(RMPermissionModel.UPDATE_TRIGGER_DATES).getActionNames().size()); + assertEquals(0, recordsManagementSecurityService.getCapability(RMPermissionModel.UPDATE_VITAL_RECORD_CYCLE_INFORMATION).getActionNames().size()); + assertEquals(0, recordsManagementSecurityService.getCapability(RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS).getActionNames().size()); + assertEquals(0, recordsManagementSecurityService.getCapability(RMPermissionModel.VIEW_RECORDS).getActionNames().size()); + assertEquals(1, recordsManagementSecurityService.getCapability(RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE).getActionNames().size()); + + } + + public void testFilePlanAsSystem() + { + Map access = recordsManagementSecurityService.getCapabilities(filePlan); + assertEquals(65, access.size()); + check(access, RMPermissionModel.ACCESS_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + check(access, RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CHANGE_OR_DELETE_REFERENCES, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_AUDIT_AS_RECORD, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.DELETE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + check(access, RMPermissionModel.DISPLAY_RIGHTS_REPORT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_SELECTION_LISTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EXPORT_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANAGE_ACCESS_CONTROLS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANAGE_ACCESS_RIGHTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANUALLY_CHANGE_DISPOSITION_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MAP_EMAIL_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.PASSWORD_CONTROL, AccessStatus.ALLOWED); + check(access, RMPermissionModel.PLANNING_REVIEW_CYCLES, AccessStatus.DENIED); + check(access, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.SELECT_AUDIT_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_CLASSIFICATION_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_TRIGGER_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_VITAL_RECORD_CYCLE_INFORMATION, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + + } + + public void testFilePlanAsAdmin() + { + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + Map access = recordsManagementSecurityService.getCapabilities(filePlan); + assertEquals(65, access.size()); + check(access, RMPermissionModel.ACCESS_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + check(access, RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CHANGE_OR_DELETE_REFERENCES, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_AUDIT_AS_RECORD, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.DELETE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + check(access, RMPermissionModel.DISPLAY_RIGHTS_REPORT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_SELECTION_LISTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EXPORT_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANAGE_ACCESS_CONTROLS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANAGE_ACCESS_RIGHTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANUALLY_CHANGE_DISPOSITION_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MAP_EMAIL_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.PASSWORD_CONTROL, AccessStatus.ALLOWED); + check(access, RMPermissionModel.PLANNING_REVIEW_CYCLES, AccessStatus.DENIED); + check(access, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.SELECT_AUDIT_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_CLASSIFICATION_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_TRIGGER_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_VITAL_RECORD_CYCLE_INFORMATION, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + + } + + public void testFilePlanAsAdministrator() + { + AuthenticationUtil.setFullyAuthenticatedUser(rm_administrator); + Map access = recordsManagementSecurityService.getCapabilities(filePlan); + assertEquals(65, access.size()); + check(access, RMPermissionModel.ACCESS_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + check(access, RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CHANGE_OR_DELETE_REFERENCES, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_AUDIT_AS_RECORD, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.DELETE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + check(access, RMPermissionModel.DISPLAY_RIGHTS_REPORT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_SELECTION_LISTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EXPORT_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANAGE_ACCESS_CONTROLS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANAGE_ACCESS_RIGHTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANUALLY_CHANGE_DISPOSITION_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MAP_EMAIL_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.PASSWORD_CONTROL, AccessStatus.ALLOWED); + check(access, RMPermissionModel.PLANNING_REVIEW_CYCLES, AccessStatus.DENIED); + check(access, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.SELECT_AUDIT_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_CLASSIFICATION_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_TRIGGER_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_VITAL_RECORD_CYCLE_INFORMATION, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + } + + public void testFilePlanAsRecordsManager() + { + Set permissions = permissionService.getAllSetPermissions(filePlan); + for (AccessPermission ap : permissions) + { + System.out.println(ap.getAuthority() + " -> " + ap.getPermission() + " (" + ap.getPosition() + ")"); + } + + AuthenticationUtil.setFullyAuthenticatedUser(rm_records_manager); + Map access = recordsManagementSecurityService.getCapabilities(filePlan); + assertEquals(65, access.size()); // 58 + File + check(access, RMPermissionModel.ACCESS_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + check(access, RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CHANGE_OR_DELETE_REFERENCES, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_AUDIT_AS_RECORD, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.DELETE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + check(access, RMPermissionModel.DISPLAY_RIGHTS_REPORT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_SELECTION_LISTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EXPORT_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANAGE_ACCESS_CONTROLS, AccessStatus.DENIED); + check(access, RMPermissionModel.MANAGE_ACCESS_RIGHTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANUALLY_CHANGE_DISPOSITION_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MAP_EMAIL_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.PASSWORD_CONTROL, AccessStatus.ALLOWED); + check(access, RMPermissionModel.PLANNING_REVIEW_CYCLES, AccessStatus.DENIED); + check(access, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.SELECT_AUDIT_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_CLASSIFICATION_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_TRIGGER_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_VITAL_RECORD_CYCLE_INFORMATION, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + + } + + public void testFilePlanAsSecurityOfficer() + { + AuthenticationUtil.setFullyAuthenticatedUser(rm_security_officer); + Map access = recordsManagementSecurityService.getCapabilities(filePlan); + assertEquals(65, access.size()); // 58 + File + check(access, RMPermissionModel.ACCESS_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + check(access, RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CHANGE_OR_DELETE_REFERENCES, AccessStatus.DENIED); + check(access, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_AUDIT_AS_RECORD, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.DELETE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + check(access, RMPermissionModel.DISPLAY_RIGHTS_REPORT, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_SELECTION_LISTS, AccessStatus.DENIED); + check(access, RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.EXPORT_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY, AccessStatus.DENIED); + check(access, RMPermissionModel.MANAGE_ACCESS_CONTROLS, AccessStatus.DENIED); + check(access, RMPermissionModel.MANAGE_ACCESS_RIGHTS, AccessStatus.DENIED); + check(access, RMPermissionModel.MANUALLY_CHANGE_DISPOSITION_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_EMAIL_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.PASSWORD_CONTROL, AccessStatus.DENIED); + check(access, RMPermissionModel.PLANNING_REVIEW_CYCLES, AccessStatus.DENIED); + check(access, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.SELECT_AUDIT_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_CLASSIFICATION_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_TRIGGER_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_VITAL_RECORD_CYCLE_INFORMATION, AccessStatus.DENIED); + check(access, RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + } + + public void testFilePlanAsPowerUser() + { + AuthenticationUtil.setFullyAuthenticatedUser(rm_power_user); + Map access = recordsManagementSecurityService.getCapabilities(filePlan); + assertEquals(65, access.size()); // 58 + File + check(access, RMPermissionModel.ACCESS_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + check(access, RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CHANGE_OR_DELETE_REFERENCES, AccessStatus.DENIED); + check(access, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_AUDIT_AS_RECORD, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.DELETE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + check(access, RMPermissionModel.DISPLAY_RIGHTS_REPORT, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_SELECTION_LISTS, AccessStatus.DENIED); + check(access, RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.EXPORT_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY, AccessStatus.DENIED); + check(access, RMPermissionModel.MANAGE_ACCESS_CONTROLS, AccessStatus.DENIED); + check(access, RMPermissionModel.MANAGE_ACCESS_RIGHTS, AccessStatus.DENIED); + check(access, RMPermissionModel.MANUALLY_CHANGE_DISPOSITION_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_EMAIL_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.PASSWORD_CONTROL, AccessStatus.DENIED); + check(access, RMPermissionModel.PLANNING_REVIEW_CYCLES, AccessStatus.DENIED); + check(access, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.SELECT_AUDIT_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_CLASSIFICATION_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_TRIGGER_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_VITAL_RECORD_CYCLE_INFORMATION, AccessStatus.DENIED); + check(access, RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + } + + public void testFilePlanAsUser() + { + AuthenticationUtil.setFullyAuthenticatedUser(rm_user); + Map access = recordsManagementSecurityService.getCapabilities(filePlan); + assertEquals(65, access.size()); // 58 + File + check(access, RMPermissionModel.ACCESS_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + check(access, RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CHANGE_OR_DELETE_REFERENCES, AccessStatus.DENIED); + check(access, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_AUDIT_AS_RECORD, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.DELETE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + check(access, RMPermissionModel.DISPLAY_RIGHTS_REPORT, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_SELECTION_LISTS, AccessStatus.DENIED); + check(access, RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.EXPORT_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY, AccessStatus.DENIED); + check(access, RMPermissionModel.MANAGE_ACCESS_CONTROLS, AccessStatus.DENIED); + check(access, RMPermissionModel.MANAGE_ACCESS_RIGHTS, AccessStatus.DENIED); + check(access, RMPermissionModel.MANUALLY_CHANGE_DISPOSITION_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_EMAIL_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.PASSWORD_CONTROL, AccessStatus.DENIED); + check(access, RMPermissionModel.PLANNING_REVIEW_CYCLES, AccessStatus.DENIED); + check(access, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.SELECT_AUDIT_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_CLASSIFICATION_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_TRIGGER_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_VITAL_RECORD_CYCLE_INFORMATION, AccessStatus.DENIED); + check(access, RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + } + + public void testRecordSeriesAsSystem() + { + Map access = recordsManagementSecurityService.getCapabilities(recordSeries); + assertEquals(65, access.size()); + check(access, RMPermissionModel.ACCESS_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + check(access, RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CHANGE_OR_DELETE_REFERENCES, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_AUDIT_AS_RECORD, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.DELETE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + check(access, RMPermissionModel.DISPLAY_RIGHTS_REPORT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_SELECTION_LISTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EXPORT_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANAGE_ACCESS_CONTROLS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANAGE_ACCESS_RIGHTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANUALLY_CHANGE_DISPOSITION_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MAP_EMAIL_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.PASSWORD_CONTROL, AccessStatus.ALLOWED); + check(access, RMPermissionModel.PLANNING_REVIEW_CYCLES, AccessStatus.DENIED); + check(access, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.SELECT_AUDIT_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_CLASSIFICATION_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_TRIGGER_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_VITAL_RECORD_CYCLE_INFORMATION, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + + } + + public void testRecordSeriesAsAdmin() + { + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + Map access = recordsManagementSecurityService.getCapabilities(recordSeries); + assertEquals(65, access.size()); + check(access, RMPermissionModel.ACCESS_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + check(access, RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CHANGE_OR_DELETE_REFERENCES, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_AUDIT_AS_RECORD, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.DELETE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + check(access, RMPermissionModel.DISPLAY_RIGHTS_REPORT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_SELECTION_LISTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EXPORT_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANAGE_ACCESS_CONTROLS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANAGE_ACCESS_RIGHTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANUALLY_CHANGE_DISPOSITION_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MAP_EMAIL_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.PASSWORD_CONTROL, AccessStatus.ALLOWED); + check(access, RMPermissionModel.PLANNING_REVIEW_CYCLES, AccessStatus.DENIED); + check(access, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.SELECT_AUDIT_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_CLASSIFICATION_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_TRIGGER_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_VITAL_RECORD_CYCLE_INFORMATION, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + + } + + public void testRecordSeriesAsAdministrator() + { + AuthenticationUtil.setFullyAuthenticatedUser(rm_administrator); + Map access = recordsManagementSecurityService.getCapabilities(recordSeries); + assertEquals(65, access.size()); + check(access, RMPermissionModel.ACCESS_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + check(access, RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CHANGE_OR_DELETE_REFERENCES, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_AUDIT_AS_RECORD, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.DELETE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + check(access, RMPermissionModel.DISPLAY_RIGHTS_REPORT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_SELECTION_LISTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EXPORT_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANAGE_ACCESS_CONTROLS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANAGE_ACCESS_RIGHTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANUALLY_CHANGE_DISPOSITION_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MAP_EMAIL_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.PASSWORD_CONTROL, AccessStatus.ALLOWED); + check(access, RMPermissionModel.PLANNING_REVIEW_CYCLES, AccessStatus.DENIED); + check(access, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.SELECT_AUDIT_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_CLASSIFICATION_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_TRIGGER_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_VITAL_RECORD_CYCLE_INFORMATION, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + } + + public void testRecordSeriesAsRecordsManager() + { + AuthenticationUtil.setFullyAuthenticatedUser(rm_records_manager); + permissionService.setPermission(recordSeries, rm_records_manager, RMPermissionModel.FILING, true); + Map access = recordsManagementSecurityService.getCapabilities(recordSeries); + assertEquals(65, access.size()); // 58 + File + check(access, RMPermissionModel.ACCESS_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + check(access, RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CHANGE_OR_DELETE_REFERENCES, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_AUDIT_AS_RECORD, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.DELETE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + check(access, RMPermissionModel.DISPLAY_RIGHTS_REPORT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_SELECTION_LISTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EXPORT_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANAGE_ACCESS_CONTROLS, AccessStatus.DENIED); + check(access, RMPermissionModel.MANAGE_ACCESS_RIGHTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANUALLY_CHANGE_DISPOSITION_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MAP_EMAIL_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.PASSWORD_CONTROL, AccessStatus.ALLOWED); + check(access, RMPermissionModel.PLANNING_REVIEW_CYCLES, AccessStatus.DENIED); + check(access, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.SELECT_AUDIT_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_CLASSIFICATION_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_TRIGGER_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_VITAL_RECORD_CYCLE_INFORMATION, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + + } + + public void testRecordSeriesAsSecurityOfficer() + { + AuthenticationUtil.setFullyAuthenticatedUser(rm_security_officer); + permissionService.setPermission(recordSeries, rm_security_officer, RMPermissionModel.FILING, true); + Map access = recordsManagementSecurityService.getCapabilities(recordSeries); + assertEquals(65, access.size()); // 58 + File + check(access, RMPermissionModel.ACCESS_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + check(access, RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CHANGE_OR_DELETE_REFERENCES, AccessStatus.DENIED); + check(access, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_AUDIT_AS_RECORD, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.DELETE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + check(access, RMPermissionModel.DISPLAY_RIGHTS_REPORT, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_SELECTION_LISTS, AccessStatus.DENIED); + check(access, RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.EXPORT_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY, AccessStatus.DENIED); + check(access, RMPermissionModel.MANAGE_ACCESS_CONTROLS, AccessStatus.DENIED); + check(access, RMPermissionModel.MANAGE_ACCESS_RIGHTS, AccessStatus.DENIED); + check(access, RMPermissionModel.MANUALLY_CHANGE_DISPOSITION_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_EMAIL_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.PASSWORD_CONTROL, AccessStatus.DENIED); + check(access, RMPermissionModel.PLANNING_REVIEW_CYCLES, AccessStatus.DENIED); + check(access, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.SELECT_AUDIT_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_CLASSIFICATION_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_TRIGGER_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_VITAL_RECORD_CYCLE_INFORMATION, AccessStatus.DENIED); + check(access, RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + } + + public void testRecordSeriesAsPowerUser() + { + AuthenticationUtil.setFullyAuthenticatedUser(rm_power_user); + permissionService.setPermission(recordSeries, rm_power_user, RMPermissionModel.FILING, true); + Map access = recordsManagementSecurityService.getCapabilities(recordSeries); + assertEquals(65, access.size()); // 58 + File + check(access, RMPermissionModel.ACCESS_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + check(access, RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CHANGE_OR_DELETE_REFERENCES, AccessStatus.DENIED); + check(access, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_AUDIT_AS_RECORD, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.DELETE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + check(access, RMPermissionModel.DISPLAY_RIGHTS_REPORT, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_SELECTION_LISTS, AccessStatus.DENIED); + check(access, RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.EXPORT_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY, AccessStatus.DENIED); + check(access, RMPermissionModel.MANAGE_ACCESS_CONTROLS, AccessStatus.DENIED); + check(access, RMPermissionModel.MANAGE_ACCESS_RIGHTS, AccessStatus.DENIED); + check(access, RMPermissionModel.MANUALLY_CHANGE_DISPOSITION_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_EMAIL_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.PASSWORD_CONTROL, AccessStatus.DENIED); + check(access, RMPermissionModel.PLANNING_REVIEW_CYCLES, AccessStatus.DENIED); + check(access, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.SELECT_AUDIT_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_CLASSIFICATION_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_TRIGGER_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_VITAL_RECORD_CYCLE_INFORMATION, AccessStatus.DENIED); + check(access, RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + } + + public void testRecordSeriesAsUser() + { + AuthenticationUtil.setFullyAuthenticatedUser(rm_user); + permissionService.setPermission(recordSeries, rm_user, RMPermissionModel.FILING, true); + Map access = recordsManagementSecurityService.getCapabilities(recordSeries); + assertEquals(65, access.size()); // 58 + File + check(access, RMPermissionModel.ACCESS_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + check(access, RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CHANGE_OR_DELETE_REFERENCES, AccessStatus.DENIED); + check(access, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_AUDIT_AS_RECORD, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.DELETE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + check(access, RMPermissionModel.DISPLAY_RIGHTS_REPORT, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_SELECTION_LISTS, AccessStatus.DENIED); + check(access, RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.EXPORT_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY, AccessStatus.DENIED); + check(access, RMPermissionModel.MANAGE_ACCESS_CONTROLS, AccessStatus.DENIED); + check(access, RMPermissionModel.MANAGE_ACCESS_RIGHTS, AccessStatus.DENIED); + check(access, RMPermissionModel.MANUALLY_CHANGE_DISPOSITION_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_EMAIL_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.PASSWORD_CONTROL, AccessStatus.DENIED); + check(access, RMPermissionModel.PLANNING_REVIEW_CYCLES, AccessStatus.DENIED); + check(access, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.SELECT_AUDIT_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_CLASSIFICATION_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_TRIGGER_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_VITAL_RECORD_CYCLE_INFORMATION, AccessStatus.DENIED); + check(access, RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + } + + public void testRecordCategoryAsSystem() + { + Map access = recordsManagementSecurityService.getCapabilities(recordCategory_1); + assertEquals(65, access.size()); + check(access, RMPermissionModel.ACCESS_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + check(access, RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CHANGE_OR_DELETE_REFERENCES, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_AUDIT_AS_RECORD, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.DELETE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + check(access, RMPermissionModel.DISPLAY_RIGHTS_REPORT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_SELECTION_LISTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EXPORT_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANAGE_ACCESS_CONTROLS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANAGE_ACCESS_RIGHTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANUALLY_CHANGE_DISPOSITION_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MAP_EMAIL_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.PASSWORD_CONTROL, AccessStatus.ALLOWED); + check(access, RMPermissionModel.PLANNING_REVIEW_CYCLES, AccessStatus.DENIED); + check(access, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.SELECT_AUDIT_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_CLASSIFICATION_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_TRIGGER_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_VITAL_RECORD_CYCLE_INFORMATION, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + + } + + public void testRecordCategoryAsAdmin() + { + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + Map access = recordsManagementSecurityService.getCapabilities(recordCategory_1); + assertEquals(65, access.size()); + check(access, RMPermissionModel.ACCESS_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + check(access, RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CHANGE_OR_DELETE_REFERENCES, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_AUDIT_AS_RECORD, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.DELETE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + check(access, RMPermissionModel.DISPLAY_RIGHTS_REPORT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_SELECTION_LISTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EXPORT_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANAGE_ACCESS_CONTROLS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANAGE_ACCESS_RIGHTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANUALLY_CHANGE_DISPOSITION_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MAP_EMAIL_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.PASSWORD_CONTROL, AccessStatus.ALLOWED); + check(access, RMPermissionModel.PLANNING_REVIEW_CYCLES, AccessStatus.DENIED); + check(access, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.SELECT_AUDIT_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_CLASSIFICATION_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_TRIGGER_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_VITAL_RECORD_CYCLE_INFORMATION, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + + } + + public void testRecordCategoryAsAdministrator() + { + AuthenticationUtil.setFullyAuthenticatedUser(rm_administrator); + Map access = recordsManagementSecurityService.getCapabilities(recordCategory_1); + assertEquals(65, access.size()); + check(access, RMPermissionModel.ACCESS_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + check(access, RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CHANGE_OR_DELETE_REFERENCES, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_AUDIT_AS_RECORD, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.DELETE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + check(access, RMPermissionModel.DISPLAY_RIGHTS_REPORT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_SELECTION_LISTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EXPORT_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANAGE_ACCESS_CONTROLS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANAGE_ACCESS_RIGHTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANUALLY_CHANGE_DISPOSITION_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MAP_EMAIL_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.PASSWORD_CONTROL, AccessStatus.ALLOWED); + check(access, RMPermissionModel.PLANNING_REVIEW_CYCLES, AccessStatus.DENIED); + check(access, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.SELECT_AUDIT_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_CLASSIFICATION_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_TRIGGER_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_VITAL_RECORD_CYCLE_INFORMATION, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + } + + public void testRecordCategoryAsRecordsManager() + { + AuthenticationUtil.setFullyAuthenticatedUser(rm_records_manager); + permissionService.setPermission(recordCategory_1, rm_records_manager, RMPermissionModel.FILING, true); + Map access = recordsManagementSecurityService.getCapabilities(recordCategory_1); + assertEquals(65, access.size()); // 58 + File + check(access, RMPermissionModel.ACCESS_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + check(access, RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CHANGE_OR_DELETE_REFERENCES, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_AUDIT_AS_RECORD, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.DELETE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + check(access, RMPermissionModel.DISPLAY_RIGHTS_REPORT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_SELECTION_LISTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EXPORT_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANAGE_ACCESS_CONTROLS, AccessStatus.DENIED); + check(access, RMPermissionModel.MANAGE_ACCESS_RIGHTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANUALLY_CHANGE_DISPOSITION_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MAP_EMAIL_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.PASSWORD_CONTROL, AccessStatus.ALLOWED); + check(access, RMPermissionModel.PLANNING_REVIEW_CYCLES, AccessStatus.DENIED); + check(access, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.SELECT_AUDIT_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_CLASSIFICATION_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_TRIGGER_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_VITAL_RECORD_CYCLE_INFORMATION, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + + } + + public void testRecordCategoryAsSecurityOfficer() + { + AuthenticationUtil.setFullyAuthenticatedUser(rm_security_officer); + permissionService.setPermission(recordCategory_1, rm_security_officer, RMPermissionModel.FILING, true); + Map access = recordsManagementSecurityService.getCapabilities(recordCategory_1); + assertEquals(65, access.size()); // 58 + File + check(access, RMPermissionModel.ACCESS_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + check(access, RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CHANGE_OR_DELETE_REFERENCES, AccessStatus.DENIED); + check(access, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_AUDIT_AS_RECORD, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.DELETE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + check(access, RMPermissionModel.DISPLAY_RIGHTS_REPORT, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_SELECTION_LISTS, AccessStatus.DENIED); + check(access, RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.EXPORT_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY, AccessStatus.DENIED); + check(access, RMPermissionModel.MANAGE_ACCESS_CONTROLS, AccessStatus.DENIED); + check(access, RMPermissionModel.MANAGE_ACCESS_RIGHTS, AccessStatus.DENIED); + check(access, RMPermissionModel.MANUALLY_CHANGE_DISPOSITION_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_EMAIL_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.PASSWORD_CONTROL, AccessStatus.DENIED); + check(access, RMPermissionModel.PLANNING_REVIEW_CYCLES, AccessStatus.DENIED); + check(access, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.SELECT_AUDIT_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_CLASSIFICATION_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_TRIGGER_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_VITAL_RECORD_CYCLE_INFORMATION, AccessStatus.DENIED); + check(access, RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + } + + public void testRecordCategoryAsPowerUser() + { + AuthenticationUtil.setFullyAuthenticatedUser(rm_power_user); + permissionService.setPermission(recordCategory_1, rm_power_user, RMPermissionModel.FILING, true); + Map access = recordsManagementSecurityService.getCapabilities(recordCategory_1); + assertEquals(65, access.size()); // 58 + File + check(access, RMPermissionModel.ACCESS_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + check(access, RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CHANGE_OR_DELETE_REFERENCES, AccessStatus.DENIED); + check(access, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_AUDIT_AS_RECORD, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.DELETE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + check(access, RMPermissionModel.DISPLAY_RIGHTS_REPORT, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_SELECTION_LISTS, AccessStatus.DENIED); + check(access, RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.EXPORT_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY, AccessStatus.DENIED); + check(access, RMPermissionModel.MANAGE_ACCESS_CONTROLS, AccessStatus.DENIED); + check(access, RMPermissionModel.MANAGE_ACCESS_RIGHTS, AccessStatus.DENIED); + check(access, RMPermissionModel.MANUALLY_CHANGE_DISPOSITION_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_EMAIL_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.PASSWORD_CONTROL, AccessStatus.DENIED); + check(access, RMPermissionModel.PLANNING_REVIEW_CYCLES, AccessStatus.DENIED); + check(access, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.SELECT_AUDIT_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_CLASSIFICATION_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_TRIGGER_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_VITAL_RECORD_CYCLE_INFORMATION, AccessStatus.DENIED); + check(access, RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + } + + public void testRecordCategoryAsUser() + { + AuthenticationUtil.setFullyAuthenticatedUser(rm_user); + permissionService.setPermission(recordCategory_1, rm_user, RMPermissionModel.FILING, true); + Map access = recordsManagementSecurityService.getCapabilities(recordCategory_1); + assertEquals(65, access.size()); // 58 + File + check(access, RMPermissionModel.ACCESS_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + check(access, RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CHANGE_OR_DELETE_REFERENCES, AccessStatus.DENIED); + check(access, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_AUDIT_AS_RECORD, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.DELETE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + check(access, RMPermissionModel.DISPLAY_RIGHTS_REPORT, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_SELECTION_LISTS, AccessStatus.DENIED); + check(access, RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.EXPORT_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY, AccessStatus.DENIED); + check(access, RMPermissionModel.MANAGE_ACCESS_CONTROLS, AccessStatus.DENIED); + check(access, RMPermissionModel.MANAGE_ACCESS_RIGHTS, AccessStatus.DENIED); + check(access, RMPermissionModel.MANUALLY_CHANGE_DISPOSITION_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_EMAIL_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.PASSWORD_CONTROL, AccessStatus.DENIED); + check(access, RMPermissionModel.PLANNING_REVIEW_CYCLES, AccessStatus.DENIED); + check(access, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.SELECT_AUDIT_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_CLASSIFICATION_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_TRIGGER_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_VITAL_RECORD_CYCLE_INFORMATION, AccessStatus.DENIED); + check(access, RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + } + + public void testRecordFolderAsSystem() + { + Map access = recordsManagementSecurityService.getCapabilities(recordFolder_1); + assertEquals(65, access.size()); + check(access, RMPermissionModel.ACCESS_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + check(access, RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CHANGE_OR_DELETE_REFERENCES, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DECLARE_AUDIT_AS_RECORD, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.DELETE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + check(access, RMPermissionModel.DISPLAY_RIGHTS_REPORT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_SELECTION_LISTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EXPORT_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANAGE_ACCESS_CONTROLS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANAGE_ACCESS_RIGHTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANUALLY_CHANGE_DISPOSITION_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MAP_EMAIL_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.PASSWORD_CONTROL, AccessStatus.ALLOWED); + check(access, RMPermissionModel.PLANNING_REVIEW_CYCLES, AccessStatus.DENIED); + check(access, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.SELECT_AUDIT_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.TRIGGER_AN_EVENT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_CLASSIFICATION_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_TRIGGER_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_VITAL_RECORD_CYCLE_INFORMATION, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + + } + + public void testRecordFolderAsAdmin() + { + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + Map access = recordsManagementSecurityService.getCapabilities(recordFolder_1); + assertEquals(65, access.size()); + check(access, RMPermissionModel.ACCESS_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + check(access, RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CHANGE_OR_DELETE_REFERENCES, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DECLARE_AUDIT_AS_RECORD, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.DELETE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + check(access, RMPermissionModel.DISPLAY_RIGHTS_REPORT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_SELECTION_LISTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EXPORT_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANAGE_ACCESS_CONTROLS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANAGE_ACCESS_RIGHTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANUALLY_CHANGE_DISPOSITION_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MAP_EMAIL_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.PASSWORD_CONTROL, AccessStatus.ALLOWED); + check(access, RMPermissionModel.PLANNING_REVIEW_CYCLES, AccessStatus.DENIED); + check(access, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.SELECT_AUDIT_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.TRIGGER_AN_EVENT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_CLASSIFICATION_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_TRIGGER_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_VITAL_RECORD_CYCLE_INFORMATION, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + + } + + public void testRecordFolderAsAdministrator() + { + AuthenticationUtil.setFullyAuthenticatedUser(rm_administrator); + Map access = recordsManagementSecurityService.getCapabilities(recordFolder_1); + assertEquals(65, access.size()); + check(access, RMPermissionModel.ACCESS_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + check(access, RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CHANGE_OR_DELETE_REFERENCES, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DECLARE_AUDIT_AS_RECORD, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.DELETE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + check(access, RMPermissionModel.DISPLAY_RIGHTS_REPORT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_SELECTION_LISTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EXPORT_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANAGE_ACCESS_CONTROLS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANAGE_ACCESS_RIGHTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANUALLY_CHANGE_DISPOSITION_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MAP_EMAIL_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.PASSWORD_CONTROL, AccessStatus.ALLOWED); + check(access, RMPermissionModel.PLANNING_REVIEW_CYCLES, AccessStatus.DENIED); + check(access, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.SELECT_AUDIT_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.TRIGGER_AN_EVENT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_CLASSIFICATION_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_TRIGGER_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_VITAL_RECORD_CYCLE_INFORMATION, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + } + + private void setFilingOnRecordFolder(NodeRef recordFolder, String authority) + { + permissionService.setPermission(recordFolder, authority, RMPermissionModel.FILING, true); + permissionService.setPermission(nodeService.getPrimaryParent(recordFolder).getParentRef(), authority, RMPermissionModel.READ_RECORDS, true); + } + + public void testRecordFolderAsRecordsManager() + { + AuthenticationUtil.setFullyAuthenticatedUser(rm_records_manager); + setFilingOnRecordFolder(recordFolder_1, rm_records_manager); + Map access = recordsManagementSecurityService.getCapabilities(recordFolder_1); + assertEquals(65, access.size()); // 58 + File + check(access, RMPermissionModel.ACCESS_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + check(access, RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CHANGE_OR_DELETE_REFERENCES, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DECLARE_AUDIT_AS_RECORD, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.DELETE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + check(access, RMPermissionModel.DISPLAY_RIGHTS_REPORT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_SELECTION_LISTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EXPORT_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANAGE_ACCESS_CONTROLS, AccessStatus.DENIED); + check(access, RMPermissionModel.MANAGE_ACCESS_RIGHTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANUALLY_CHANGE_DISPOSITION_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MAP_EMAIL_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.PASSWORD_CONTROL, AccessStatus.ALLOWED); + check(access, RMPermissionModel.PLANNING_REVIEW_CYCLES, AccessStatus.DENIED); + check(access, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.SELECT_AUDIT_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.TRIGGER_AN_EVENT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_CLASSIFICATION_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_TRIGGER_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_VITAL_RECORD_CYCLE_INFORMATION, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + + } + + public void testRecordFolderAsSecurityOfficer() + { + AuthenticationUtil.setFullyAuthenticatedUser(rm_security_officer); + permissionService.setPermission(recordFolder_1, rm_security_officer, RMPermissionModel.FILING, true); + permissionService.setPermission(nodeService.getPrimaryParent(recordFolder_1).getParentRef(), rm_security_officer, RMPermissionModel.READ_RECORDS, true); + Map access = recordsManagementSecurityService.getCapabilities(recordFolder_1); + assertEquals(65, access.size()); // 58 + File + check(access, RMPermissionModel.ACCESS_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + check(access, RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CHANGE_OR_DELETE_REFERENCES, AccessStatus.DENIED); + check(access, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DECLARE_AUDIT_AS_RECORD, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.DELETE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + check(access, RMPermissionModel.DISPLAY_RIGHTS_REPORT, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_SELECTION_LISTS, AccessStatus.DENIED); + check(access, RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.EXPORT_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY, AccessStatus.DENIED); + check(access, RMPermissionModel.MANAGE_ACCESS_CONTROLS, AccessStatus.DENIED); + check(access, RMPermissionModel.MANAGE_ACCESS_RIGHTS, AccessStatus.DENIED); + check(access, RMPermissionModel.MANUALLY_CHANGE_DISPOSITION_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_EMAIL_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.PASSWORD_CONTROL, AccessStatus.DENIED); + check(access, RMPermissionModel.PLANNING_REVIEW_CYCLES, AccessStatus.DENIED); + check(access, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.SELECT_AUDIT_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_CLASSIFICATION_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_TRIGGER_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_VITAL_RECORD_CYCLE_INFORMATION, AccessStatus.DENIED); + check(access, RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + } + + public void testRecordFolderAsPowerUser() + { + AuthenticationUtil.setFullyAuthenticatedUser(rm_power_user); + permissionService.setPermission(recordFolder_1, rm_power_user, RMPermissionModel.FILING, true); + permissionService.setPermission(nodeService.getPrimaryParent(recordFolder_1).getParentRef(), rm_power_user, RMPermissionModel.READ_RECORDS, true); + Map access = recordsManagementSecurityService.getCapabilities(recordFolder_1); + assertEquals(65, access.size()); // 58 + File + check(access, RMPermissionModel.ACCESS_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + check(access, RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CHANGE_OR_DELETE_REFERENCES, AccessStatus.DENIED); + check(access, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DECLARE_AUDIT_AS_RECORD, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.DELETE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + check(access, RMPermissionModel.DISPLAY_RIGHTS_REPORT, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_SELECTION_LISTS, AccessStatus.DENIED); + check(access, RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.EXPORT_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY, AccessStatus.DENIED); + check(access, RMPermissionModel.MANAGE_ACCESS_CONTROLS, AccessStatus.DENIED); + check(access, RMPermissionModel.MANAGE_ACCESS_RIGHTS, AccessStatus.DENIED); + check(access, RMPermissionModel.MANUALLY_CHANGE_DISPOSITION_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_EMAIL_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.PASSWORD_CONTROL, AccessStatus.DENIED); + check(access, RMPermissionModel.PLANNING_REVIEW_CYCLES, AccessStatus.DENIED); + check(access, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.SELECT_AUDIT_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_CLASSIFICATION_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_TRIGGER_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_VITAL_RECORD_CYCLE_INFORMATION, AccessStatus.DENIED); + check(access, RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + } + + public void testRecordFolderAsUser() + { + AuthenticationUtil.setFullyAuthenticatedUser(rm_user); + setFilingOnRecordFolder(recordFolder_1, rm_user); + Map access = recordsManagementSecurityService.getCapabilities(recordFolder_1); + assertEquals(65, access.size()); // 58 + File + check(access, RMPermissionModel.ACCESS_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + check(access, RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CHANGE_OR_DELETE_REFERENCES, AccessStatus.DENIED); + check(access, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_AUDIT_AS_RECORD, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.DELETE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + check(access, RMPermissionModel.DISPLAY_RIGHTS_REPORT, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_SELECTION_LISTS, AccessStatus.DENIED); + check(access, RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.EXPORT_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY, AccessStatus.DENIED); + check(access, RMPermissionModel.MANAGE_ACCESS_CONTROLS, AccessStatus.DENIED); + check(access, RMPermissionModel.MANAGE_ACCESS_RIGHTS, AccessStatus.DENIED); + check(access, RMPermissionModel.MANUALLY_CHANGE_DISPOSITION_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_EMAIL_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.PASSWORD_CONTROL, AccessStatus.DENIED); + check(access, RMPermissionModel.PLANNING_REVIEW_CYCLES, AccessStatus.DENIED); + check(access, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.SELECT_AUDIT_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_CLASSIFICATION_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_TRIGGER_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_VITAL_RECORD_CYCLE_INFORMATION, AccessStatus.DENIED); + check(access, RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + } + + public void testRecordAsSystem() + { + Map access = recordsManagementSecurityService.getCapabilities(record_1); + assertEquals(65, access.size()); + check(access, RMPermissionModel.ACCESS_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + check(access, RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CHANGE_OR_DELETE_REFERENCES, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DECLARE_AUDIT_AS_RECORD, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.DELETE_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + check(access, RMPermissionModel.DISPLAY_RIGHTS_REPORT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EDIT_SELECTION_LISTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EXPORT_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANAGE_ACCESS_CONTROLS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANAGE_ACCESS_RIGHTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANUALLY_CHANGE_DISPOSITION_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MAP_EMAIL_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.PASSWORD_CONTROL, AccessStatus.ALLOWED); + check(access, RMPermissionModel.PLANNING_REVIEW_CYCLES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.SELECT_AUDIT_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.TRIGGER_AN_EVENT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_CLASSIFICATION_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_TRIGGER_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_VITAL_RECORD_CYCLE_INFORMATION, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + + } + + public void testRecordAsAdmin() + { + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + Map access = recordsManagementSecurityService.getCapabilities(record_1); + assertEquals(65, access.size()); + check(access, RMPermissionModel.ACCESS_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + check(access, RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CHANGE_OR_DELETE_REFERENCES, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DECLARE_AUDIT_AS_RECORD, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.DELETE_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + check(access, RMPermissionModel.DISPLAY_RIGHTS_REPORT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EDIT_SELECTION_LISTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EXPORT_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANAGE_ACCESS_CONTROLS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANAGE_ACCESS_RIGHTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANUALLY_CHANGE_DISPOSITION_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MAP_EMAIL_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.PASSWORD_CONTROL, AccessStatus.ALLOWED); + check(access, RMPermissionModel.PLANNING_REVIEW_CYCLES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.SELECT_AUDIT_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.TRIGGER_AN_EVENT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_CLASSIFICATION_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_TRIGGER_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_VITAL_RECORD_CYCLE_INFORMATION, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + + } + + public void testRecordAsAdministrator() + { + AuthenticationUtil.setFullyAuthenticatedUser(rm_administrator); + Map access = recordsManagementSecurityService.getCapabilities(record_1); + assertEquals(65, access.size()); + check(access, RMPermissionModel.ACCESS_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + check(access, RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CHANGE_OR_DELETE_REFERENCES, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DECLARE_AUDIT_AS_RECORD, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.DELETE_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + check(access, RMPermissionModel.DISPLAY_RIGHTS_REPORT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EDIT_SELECTION_LISTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EXPORT_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANAGE_ACCESS_CONTROLS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANAGE_ACCESS_RIGHTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANUALLY_CHANGE_DISPOSITION_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MAP_EMAIL_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.PASSWORD_CONTROL, AccessStatus.ALLOWED); + check(access, RMPermissionModel.PLANNING_REVIEW_CYCLES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.SELECT_AUDIT_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.TRIGGER_AN_EVENT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_CLASSIFICATION_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_TRIGGER_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_VITAL_RECORD_CYCLE_INFORMATION, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + } + + public void testRecordAsRecordsManager() + { + AuthenticationUtil.setFullyAuthenticatedUser(rm_records_manager); + setFilingOnRecord(record_1, rm_records_manager); + Map access = recordsManagementSecurityService.getCapabilities(record_1); + assertEquals(65, access.size()); // 58 + File + check(access, RMPermissionModel.ACCESS_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + check(access, RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CHANGE_OR_DELETE_REFERENCES, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DECLARE_AUDIT_AS_RECORD, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.DELETE_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + check(access, RMPermissionModel.DISPLAY_RIGHTS_REPORT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EDIT_SELECTION_LISTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EXPORT_AUDIT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANAGE_ACCESS_CONTROLS, AccessStatus.DENIED); + check(access, RMPermissionModel.MANAGE_ACCESS_RIGHTS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MANUALLY_CHANGE_DISPOSITION_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MAP_EMAIL_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.PASSWORD_CONTROL, AccessStatus.ALLOWED); + check(access, RMPermissionModel.PLANNING_REVIEW_CYCLES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.SELECT_AUDIT_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.TRIGGER_AN_EVENT, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_CLASSIFICATION_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_TRIGGER_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_VITAL_RECORD_CYCLE_INFORMATION, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + + } + + public void testRecordAsSecurityOfficer() + { + AuthenticationUtil.setFullyAuthenticatedUser(rm_security_officer); + setFilingOnRecord(record_1, rm_security_officer); + Map access = recordsManagementSecurityService.getCapabilities(record_1); + assertEquals(65, access.size()); // 58 + File + check(access, RMPermissionModel.ACCESS_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + check(access, RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CHANGE_OR_DELETE_REFERENCES, AccessStatus.DENIED); + check(access, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DECLARE_AUDIT_AS_RECORD, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.DELETE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + check(access, RMPermissionModel.DISPLAY_RIGHTS_REPORT, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EDIT_SELECTION_LISTS, AccessStatus.DENIED); + check(access, RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.EXPORT_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY, AccessStatus.DENIED); + check(access, RMPermissionModel.MANAGE_ACCESS_CONTROLS, AccessStatus.DENIED); + check(access, RMPermissionModel.MANAGE_ACCESS_RIGHTS, AccessStatus.DENIED); + check(access, RMPermissionModel.MANUALLY_CHANGE_DISPOSITION_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_EMAIL_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.PASSWORD_CONTROL, AccessStatus.DENIED); + check(access, RMPermissionModel.PLANNING_REVIEW_CYCLES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.SELECT_AUDIT_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_CLASSIFICATION_DATES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.UPDATE_TRIGGER_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_VITAL_RECORD_CYCLE_INFORMATION, AccessStatus.DENIED); + check(access, RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + } + + private void setFilingOnRecord(NodeRef record, String authority) + { + NodeRef recordFolder = nodeService.getPrimaryParent(record).getParentRef(); + permissionService.setPermission(recordFolder, authority, RMPermissionModel.FILING, true); + permissionService.setPermission(nodeService.getPrimaryParent(recordFolder).getParentRef(), authority, RMPermissionModel.READ_RECORDS, true); + } + + public void testRecordAsPowerUser() + { + AuthenticationUtil.setFullyAuthenticatedUser(rm_power_user); + setFilingOnRecord(record_1, rm_power_user); + Map access = recordsManagementSecurityService.getCapabilities(record_1); + assertEquals(65, access.size()); // 58 + File + check(access, RMPermissionModel.ACCESS_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + check(access, RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CHANGE_OR_DELETE_REFERENCES, AccessStatus.DENIED); + check(access, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.DECLARE_AUDIT_AS_RECORD, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.DELETE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + check(access, RMPermissionModel.DISPLAY_RIGHTS_REPORT, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.ALLOWED); + check(access, RMPermissionModel.EDIT_SELECTION_LISTS, AccessStatus.DENIED); + check(access, RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.EXPORT_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY, AccessStatus.DENIED); + check(access, RMPermissionModel.MANAGE_ACCESS_CONTROLS, AccessStatus.DENIED); + check(access, RMPermissionModel.MANAGE_ACCESS_RIGHTS, AccessStatus.DENIED); + check(access, RMPermissionModel.MANUALLY_CHANGE_DISPOSITION_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_EMAIL_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.PASSWORD_CONTROL, AccessStatus.DENIED); + check(access, RMPermissionModel.PLANNING_REVIEW_CYCLES, AccessStatus.ALLOWED); + check(access, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.SELECT_AUDIT_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_CLASSIFICATION_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_TRIGGER_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_VITAL_RECORD_CYCLE_INFORMATION, AccessStatus.DENIED); + check(access, RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + } + + public void testRecordAsUser() + { + AuthenticationUtil.setFullyAuthenticatedUser(rm_user); + Map access = recordsManagementSecurityService.getCapabilities(record_1); + assertEquals(65, access.size()); // 58 + File + check(access, RMPermissionModel.ACCESS_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + check(access, RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CHANGE_OR_DELETE_REFERENCES, AccessStatus.DENIED); + check(access, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, AccessStatus.DENIED); + check(access, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_AUDIT_AS_RECORD, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.DELETE_LINKS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.DELETE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + check(access, RMPermissionModel.DISPLAY_RIGHTS_REPORT, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.EDIT_SELECTION_LISTS, AccessStatus.DENIED); + check(access, RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES, AccessStatus.DENIED); + check(access, RMPermissionModel.EXPORT_AUDIT, AccessStatus.DENIED); + check(access, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY, AccessStatus.DENIED); + check(access, RMPermissionModel.MANAGE_ACCESS_CONTROLS, AccessStatus.DENIED); + check(access, RMPermissionModel.MANAGE_ACCESS_RIGHTS, AccessStatus.DENIED); + check(access, RMPermissionModel.MANUALLY_CHANGE_DISPOSITION_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.MAP_EMAIL_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.MOVE_RECORDS, AccessStatus.UNDETERMINED); + check(access, RMPermissionModel.PASSWORD_CONTROL, AccessStatus.DENIED); + check(access, RMPermissionModel.PLANNING_REVIEW_CYCLES, AccessStatus.DENIED); + check(access, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + check(access, RMPermissionModel.SELECT_AUDIT_METADATA, AccessStatus.DENIED); + check(access, RMPermissionModel.TRIGGER_AN_EVENT, AccessStatus.DENIED); + check(access, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_CLASSIFICATION_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_TRIGGER_DATES, AccessStatus.DENIED); + check(access, RMPermissionModel.UPDATE_VITAL_RECORD_CYCLE_INFORMATION, AccessStatus.DENIED); + check(access, RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, AccessStatus.DENIED); + check(access, RMPermissionModel.VIEW_RECORDS, AccessStatus.ALLOWED); + check(access, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + } + + private void checkCapability(String user, NodeRef nodeRef, String permission, AccessStatus accessStstus) + { + AuthenticationUtil.setFullyAuthenticatedUser(user); + Map access = capabilityService.getCapabilitiesAccessState(nodeRef); + check(access, permission, accessStstus); + } + + private void checkPermission(String user, NodeRef nodeRef, String permission, AccessStatus accessStstus) + { + AuthenticationUtil.setFullyAuthenticatedUser(user); + assertTrue(permissionService.hasPermission(nodeRef, permission) == accessStstus); + } + + public void testAccessAuditCapability() + { + // capability is checked above - just check permission assignments + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.ACCESS_AUDIT, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.ACCESS_AUDIT, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.ACCESS_AUDIT, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.ACCESS_AUDIT, AccessStatus.DENIED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.ACCESS_AUDIT, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.ACCESS_AUDIT, AccessStatus.DENIED); + } + + public void testAddModifyEventDatesCapability() + { + // Folder + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); + checkPermission(rm_user, filePlan, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + + // folder level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); + checkCapability(rm_administrator, recordFolder_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, recordFolder_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, recordFolder_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); + checkCapability(rm_power_user, recordFolder_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); + checkCapability(rm_user, recordFolder_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(rm_administrator, record_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(rm_records_manager, record_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(rm_security_officer, record_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(rm_power_user, record_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(rm_user, record_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + + // record level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(rm_administrator, recordFolder_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(rm_records_manager, recordFolder_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(rm_security_officer, recordFolder_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); + checkCapability(rm_administrator, record_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, record_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, record_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); + checkCapability(rm_power_user, record_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); + checkCapability(rm_user, record_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + + // check person with no access and add read and write + // Filing + + checkCapability(test_user, recordFolder_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS, true); + permissionService.setInheritParentPermissions(recordCategory_1, false); + permissionService.setInheritParentPermissions(recordCategory_2, false); + permissionService.setPermission(recordCategory_1, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordCategory_2, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.DECLARE_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.ADD_MODIFY_EVENT_DATES, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.DECLARE_RECORDS); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.DECLARE_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); + + permissionService.deletePermission(recordFolder_1, testers, RMPermissionModel.FILING); + permissionService.deletePermission(recordFolder_2, testers, RMPermissionModel.FILING); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); + + // check frozen + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + Map params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "one"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "freeze", params); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "Two"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "freeze", params); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "unfreeze"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "unfreeze"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); + + // Check closed + // should make no difference + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "closeRecordFolder"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "closeRecordFolder"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "openRecordFolder"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "openRecordFolder"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); + + // try and complete some events + + AuthenticationUtil.setFullyAuthenticatedUser(test_user); + Map eventDetails = new HashMap(3); + eventDetails.put(CompleteEventAction.PARAM_EVENT_NAME, "event"); + eventDetails.put(CompleteEventAction.PARAM_EVENT_COMPLETED_AT, new Date()); + eventDetails.put(CompleteEventAction.PARAM_EVENT_COMPLETED_BY, test_user); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "completeEvent", eventDetails); + try + { + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "completeEvent", eventDetails); + fail(); + } + catch (AccessDeniedException ade) + { + + } + try + { + recordsManagementActionService.executeRecordsManagementAction(record_1, "completeEvent", eventDetails); + fail(); + } + catch (AccessDeniedException ade) + { + + } + + recordsManagementActionService.executeRecordsManagementAction(record_2, "completeEvent", eventDetails); + + // check protected properties + + try + { + publicNodeService.setProperty(record_1, RecordsManagementModel.PROP_EVENT_EXECUTION_COMPLETE, true); + fail(); + } + catch (AccessDeniedException ade) + { + + } + try + { + publicNodeService.setProperty(record_1, RecordsManagementModel.PROP_EVENT_EXECUTION_COMPLETED_AT, new Date()); + fail(); + } + catch (AccessDeniedException ade) + { + + } + try + { + publicNodeService.setProperty(record_1, RecordsManagementModel.PROP_EVENT_EXECUTION_COMPLETED_BY, "me"); + fail(); + } + catch (AccessDeniedException ade) + { + + } + + // check cutoff + + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.HOUR, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + + nodeService.setProperty(record_1, RecordsManagementModel.PROP_ORIGINATOR, "origValue"); + nodeService.setProperty(record_1, RecordsManagementModel.PROP_ORIGINATING_ORGANIZATION, "origOrgValue"); + nodeService.setProperty(record_1, RecordsManagementModel.PROP_PUBLICATION_DATE, new Date()); + nodeService.setProperty(record_1, ContentModel.PROP_TITLE, "titleValue"); + recordsManagementActionService.executeRecordsManagementAction(record_1, "declareRecord"); + + nodeService.setProperty(record_2, RecordsManagementModel.PROP_ORIGINATOR, "origValue"); + nodeService.setProperty(record_2, RecordsManagementModel.PROP_ORIGINATING_ORGANIZATION, "origOrgValue"); + nodeService.setProperty(record_2, RecordsManagementModel.PROP_PUBLICATION_DATE, new Date()); + nodeService.setProperty(record_2, ContentModel.PROP_TITLE, "titleValue"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "declareRecord"); + + NodeRef ndNodeRef = this.nodeService.getChildAssocs(recordFolder_1, RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + this.nodeService.setProperty(ndNodeRef, RecordsManagementModel.PROP_DISPOSITION_AS_OF, calendar.getTime()); + ndNodeRef = this.nodeService.getChildAssocs(record_2, RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + this.nodeService.setProperty(ndNodeRef, RecordsManagementModel.PROP_DISPOSITION_AS_OF, calendar.getTime()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "cutoff", null); + recordsManagementActionService.executeRecordsManagementAction(record_2, "cutoff", null); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); + } + + public void testApproveRecordsScheduledForCutoffCapability() + { + // Folder + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + + // folder level - not eligible all deny + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(rm_administrator, recordFolder_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(rm_records_manager, recordFolder_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(rm_security_officer, recordFolder_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(rm_administrator, record_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(rm_records_manager, record_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(rm_security_officer, record_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(rm_power_user, record_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(rm_user, record_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + + // record level - not eligible all deny + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(rm_administrator, recordFolder_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(rm_records_manager, recordFolder_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(rm_security_officer, recordFolder_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(rm_administrator, record_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(rm_records_manager, record_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(rm_security_officer, record_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(rm_power_user, record_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(rm_user, record_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + + // Set appropriate state - declare records and make eligible + + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.HOUR, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + + nodeService.setProperty(record_1, RecordsManagementModel.PROP_ORIGINATOR, "origValue"); + nodeService.setProperty(record_1, RecordsManagementModel.PROP_ORIGINATING_ORGANIZATION, "origOrgValue"); + nodeService.setProperty(record_1, RecordsManagementModel.PROP_PUBLICATION_DATE, new Date()); + nodeService.setProperty(record_1, ContentModel.PROP_TITLE, "titleValue"); + recordsManagementActionService.executeRecordsManagementAction(record_1, "declareRecord"); + + nodeService.setProperty(record_2, RecordsManagementModel.PROP_ORIGINATOR, "origValue"); + nodeService.setProperty(record_2, RecordsManagementModel.PROP_ORIGINATING_ORGANIZATION, "origOrgValue"); + nodeService.setProperty(record_2, RecordsManagementModel.PROP_PUBLICATION_DATE, new Date()); + nodeService.setProperty(record_2, ContentModel.PROP_TITLE, "titleValue"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "declareRecord"); + + NodeRef ndNodeRef = this.nodeService.getChildAssocs(recordFolder_1, RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + this.nodeService.setProperty(ndNodeRef, RecordsManagementModel.PROP_DISPOSITION_AS_OF, calendar.getTime()); + ndNodeRef = this.nodeService.getChildAssocs(record_2, RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + this.nodeService.setProperty(ndNodeRef, RecordsManagementModel.PROP_DISPOSITION_AS_OF, calendar.getTime()); + + // folder level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.ALLOWED); + checkCapability(rm_administrator, recordFolder_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, recordFolder_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, recordFolder_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(rm_administrator, record_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(rm_records_manager, record_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(rm_security_officer, record_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(rm_power_user, record_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(rm_user, record_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + + // record level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(rm_administrator, recordFolder_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(rm_records_manager, recordFolder_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(rm_security_officer, recordFolder_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.ALLOWED); + checkCapability(rm_administrator, record_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, record_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, record_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(rm_power_user, record_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(rm_user, record_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + + // check person with no access and add read and write + // Filing + + checkCapability(test_user, recordFolder_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS, true); + permissionService.setInheritParentPermissions(recordCategory_1, false); + permissionService.setInheritParentPermissions(recordCategory_2, false); + permissionService.setPermission(recordCategory_1, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordCategory_2, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.DECLARE_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.ALLOWED); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.DECLARE_RECORDS); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.ALLOWED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.DECLARE_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.ALLOWED); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.ALLOWED); + + permissionService.deletePermission(recordFolder_1, testers, RMPermissionModel.FILING); + permissionService.deletePermission(recordFolder_2, testers, RMPermissionModel.FILING); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.ALLOWED); + + // check frozen + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + Map params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "one"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "freeze", params); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.ALLOWED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "Two"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "freeze", params); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "unfreeze"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "unfreeze"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.ALLOWED); + + // Check closed + // should make no difference + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "closeRecordFolder"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "closeRecordFolder"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.ALLOWED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "openRecordFolder"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "openRecordFolder"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.ALLOWED); + + // try and cut off + + AuthenticationUtil.setFullyAuthenticatedUser(test_user); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "cutoff", null); + try + { + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "cutoff", null); + fail(); + } + catch (AccessDeniedException ade) + { + + } + try + { + recordsManagementActionService.executeRecordsManagementAction(record_1, "cutoff", null); + fail(); + } + catch (AccessDeniedException ade) + { + + } + recordsManagementActionService.executeRecordsManagementAction(record_2, "cutoff", null); + + // check protected properties + + try + { + publicNodeService.setProperty(record_1, RecordsManagementModel.PROP_CUT_OFF_DATE, new Date()); + fail(); + } + catch (AccessDeniedException ade) + { + + } + + // check cutoff again (it is already cut off) + + // try + // { + // recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "cutoff", null); + // fail(); + // } + // catch (AccessDeniedException ade) + // { + // + // } + // try + // { + // recordsManagementActionService.executeRecordsManagementAction(record_2, "cutoff", null); + // fail(); + // } + // catch (AccessDeniedException ade) + // { + // + // } + + // checkCapability(test_user, recordFolder_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + // AccessStatus.DENIED); + // checkCapability(test_user, record_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + // AccessStatus.DENIED); + // checkCapability(test_user, recordFolder_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + // AccessStatus.DENIED); + // checkCapability(test_user, record_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, + // AccessStatus.DENIED); + } + + public void testAttachRulesToMetadataPropertiesCapability() + { + // capability is checked above - just check permission assignments + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES, AccessStatus.DENIED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.ATTACH_RULES_TO_METADATA_PROPERTIES, AccessStatus.DENIED); + } + + private void setupForTransfer() + { + // Folder + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + + // folder level - not eligible all deny + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_administrator, recordFolder_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_records_manager, recordFolder_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_security_officer, recordFolder_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_administrator, record_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_records_manager, record_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_security_officer, record_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_power_user, record_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_user, record_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + + // record level - not eligible all deny + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_administrator, recordFolder_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_records_manager, recordFolder_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_security_officer, recordFolder_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_administrator, record_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_records_manager, record_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_security_officer, record_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_power_user, record_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_user, record_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + + // Set appropriate state - declare records and make eligible + + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.HOUR, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + + nodeService.setProperty(record_1, RecordsManagementModel.PROP_ORIGINATOR, "origValue"); + nodeService.setProperty(record_1, RecordsManagementModel.PROP_ORIGINATING_ORGANIZATION, "origOrgValue"); + nodeService.setProperty(record_1, RecordsManagementModel.PROP_PUBLICATION_DATE, new Date()); + nodeService.setProperty(record_1, ContentModel.PROP_TITLE, "titleValue"); + recordsManagementActionService.executeRecordsManagementAction(record_1, "declareRecord"); + + nodeService.setProperty(record_2, RecordsManagementModel.PROP_ORIGINATOR, "origValue"); + nodeService.setProperty(record_2, RecordsManagementModel.PROP_ORIGINATING_ORGANIZATION, "origOrgValue"); + nodeService.setProperty(record_2, RecordsManagementModel.PROP_PUBLICATION_DATE, new Date()); + nodeService.setProperty(record_2, ContentModel.PROP_TITLE, "titleValue"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "declareRecord"); + + NodeRef ndNodeRef = this.nodeService.getChildAssocs(recordFolder_1, RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + this.nodeService.setProperty(ndNodeRef, RecordsManagementModel.PROP_DISPOSITION_AS_OF, calendar.getTime()); + ndNodeRef = this.nodeService.getChildAssocs(record_2, RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + this.nodeService.setProperty(ndNodeRef, RecordsManagementModel.PROP_DISPOSITION_AS_OF, calendar.getTime()); + + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "cutoff", null); + recordsManagementActionService.executeRecordsManagementAction(record_2, "cutoff", null); + + ndNodeRef = this.nodeService.getChildAssocs(recordFolder_1, RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + this.nodeService.setProperty(ndNodeRef, RecordsManagementModel.PROP_DISPOSITION_AS_OF, calendar.getTime()); + ndNodeRef = this.nodeService.getChildAssocs(record_2, RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + this.nodeService.setProperty(ndNodeRef, RecordsManagementModel.PROP_DISPOSITION_AS_OF, calendar.getTime()); + + // folder level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.ALLOWED); + checkCapability(rm_administrator, recordFolder_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, recordFolder_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, recordFolder_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_administrator, record_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_records_manager, record_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_security_officer, record_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_power_user, record_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_user, record_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + + // record level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_administrator, recordFolder_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_records_manager, recordFolder_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_security_officer, recordFolder_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.ALLOWED); + checkCapability(rm_administrator, record_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, record_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, record_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_power_user, record_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_user, record_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + + // check person with no access and add read and write + // Filing + + checkCapability(test_user, recordFolder_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS, true); + permissionService.setInheritParentPermissions(recordCategory_1, false); + permissionService.setInheritParentPermissions(recordCategory_2, false); + permissionService.setPermission(recordCategory_1, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordCategory_2, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.DECLARE_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.ALLOWED); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.DECLARE_RECORDS); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.ALLOWED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.DECLARE_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.ALLOWED); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.ALLOWED); + + permissionService.deletePermission(recordFolder_1, testers, RMPermissionModel.FILING); + permissionService.deletePermission(recordFolder_2, testers, RMPermissionModel.FILING); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.ALLOWED); + + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.ALLOWED); + + // check frozen + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + Map params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "one"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "freeze", params); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.ALLOWED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "Two"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "freeze", params); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "unfreeze"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "unfreeze"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.ALLOWED); + + // Check closed + // should make no difference + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "closeRecordFolder"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "closeRecordFolder"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.ALLOWED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "openRecordFolder"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "openRecordFolder"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.ALLOWED); + } + + private void setupForTransferComplete() + { + checkCapability(test_user, recordFolder_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.AUTHORIZE_ALL_TRANSFERS, AccessStatus.ALLOWED); + + // check each action + + TransferAction transfer = (TransferAction) ctx.getBean("transfer"); + assertFalse(transfer.isExecutable(recordFolder_1, null)); + assertFalse(transfer.isExecutable(record_1, null)); + assertFalse(transfer.isExecutable(recordFolder_2, null)); + assertFalse(transfer.isExecutable(record_2, null)); + + TransferCompleteAction transferComplete = (TransferCompleteAction) ctx.getBean("transferComplete"); + assertTrue(transferComplete.isExecutable(recordFolder_1, null)); + assertFalse(transferComplete.isExecutable(record_1, null)); + assertFalse(transferComplete.isExecutable(recordFolder_2, null)); + assertTrue(transferComplete.isExecutable(record_2, null)); + } + + public void testAuthorizeAllTransfersCapability() + { + setupForTransfer(); + + // try and transfer + + AuthenticationUtil.setFullyAuthenticatedUser(test_user); + + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "transfer", null); + + recordsManagementActionService.executeRecordsManagementAction(record_2, "transfer", null); + + setupForTransferComplete(); + + // try and complete the transfer + + AuthenticationUtil.setFullyAuthenticatedUser(test_user); + + recordsManagementActionService.executeRecordsManagementAction(getTransferObject(recordFolder_1), "transferComplete", null); + } + + public void testAuthorizeAllTransfersCapability_TransferNegative() + { + setupForTransfer(); + + // try and transfer + + AuthenticationUtil.setFullyAuthenticatedUser(test_user); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "transfer", null); + + recordsManagementActionService.executeRecordsManagementAction(record_2, "transfer", null); + + // -ve checks (ALF-2749) + // note: ideally, each -ve test should be run independently (if we want outer/setup txn to rollback) + + try + { + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "transfer", null); + fail(); + } + catch (AccessDeniedException ade) + { + + } + try + { + recordsManagementActionService.executeRecordsManagementAction(record_1, "transfer", null); + fail(); + } + catch (AccessDeniedException ade) + { + + } + + // check protected properties + + // PROP_DISPOSITION_ACTION_STARTED_AT + // PROP_DISPOSITION_ACTION_STARTED_BY + // PROP_DISPOSITION_ACTION_COMPLETED_AT + // PROP_DISPOSITION_ACTION_COMPLETED_BY + + try + { + publicNodeService.setProperty(record_1, RecordsManagementModel.PROP_DISPOSITION_ACTION_STARTED_AT, true); + fail(); + } + catch (AccessDeniedException ade) + { + + } + + try + { + publicNodeService.setProperty(record_1, RecordsManagementModel.PROP_DISPOSITION_ACTION_STARTED_BY, true); + fail(); + } + catch (AccessDeniedException ade) + { + + } + + try + { + publicNodeService.setProperty(record_1, RecordsManagementModel.PROP_DISPOSITION_ACTION_COMPLETED_AT, true); + fail(); + } + catch (AccessDeniedException ade) + { + + } + + try + { + publicNodeService.setProperty(record_1, RecordsManagementModel.PROP_DISPOSITION_ACTION_COMPLETED_BY, true); + fail(); + } + catch (AccessDeniedException ade) + { + + } + + // check cutoff again (it is already cut off) + + try + { + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "transfer", null); + fail(); + } + catch (AccessDeniedException ade) + { + + } + catch (AlfrescoRuntimeException are) + { + + } + try + { + recordsManagementActionService.executeRecordsManagementAction(record_2, "transfer", null); + fail(); + } + catch (AccessDeniedException ade) + { + + } + catch (AlfrescoRuntimeException are) + { + + } + } + + public void testAuthorizeAllTransfersCapability_TransferCompleteNegative() + { + setupForTransfer(); + + // try and transfer + + AuthenticationUtil.setFullyAuthenticatedUser(test_user); + + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "transfer", null); + + recordsManagementActionService.executeRecordsManagementAction(record_2, "transfer", null); + + setupForTransferComplete(); + + // try and complete the transfer + + AuthenticationUtil.setFullyAuthenticatedUser(test_user); + + recordsManagementActionService.executeRecordsManagementAction(getTransferObject(recordFolder_1), "transferComplete", null); + + // -ve checks (ALF-2749) + // note: ideally, each -ve test should be run independently (if we want outer/setup txn to rollback) + + try + { + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "transferComplete", null); + fail(); + } + catch (AccessDeniedException ade) + { + + } + try + { + recordsManagementActionService.executeRecordsManagementAction(record_1, "transferComplete", null); + fail(); + } + catch (AccessDeniedException ade) + { + + } + try + { + // will fail as this is in the same transafer which is now done. + recordsManagementActionService.executeRecordsManagementAction(getTransferObject(record_2), "transferComplete", null); + fail(); + } + catch (AccessDeniedException ade) + { + + } + + // try again - should fail + + try + { + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "transferComplete", null); + fail(); + } + catch (AccessDeniedException ade) + { + + } + try + { + recordsManagementActionService.executeRecordsManagementAction(record_2, "transferComplete", null); + fail(); + } + catch (AccessDeniedException ade) + { + + } + } + + + private NodeRef getTransferObject(NodeRef fp) + { + List assocs = this.nodeService.getParentAssocs(fp, RecordsManagementModel.ASSOC_TRANSFERRED, RegexQNamePattern.MATCH_ALL); + if (assocs.size() > 0) + { + return assocs.get(0).getParentRef(); + } + else + { + return fp; + } + } + + private void setupForAccession() + { + // Folder + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + + // folder level - not eligible all deny + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_administrator, recordFolder_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_records_manager, recordFolder_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_security_officer, recordFolder_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_administrator, record_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_records_manager, record_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_security_officer, record_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_power_user, record_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_user, record_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + + // record level - not eligible all deny + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_administrator, recordFolder_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_records_manager, recordFolder_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_security_officer, recordFolder_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_administrator, record_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_records_manager, record_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_security_officer, record_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_power_user, record_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_user, record_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + + // Set appropriate state - declare records and make eligible + + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.HOUR, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + + nodeService.setProperty(record_1, RecordsManagementModel.PROP_ORIGINATOR, "origValue"); + nodeService.setProperty(record_1, RecordsManagementModel.PROP_ORIGINATING_ORGANIZATION, "origOrgValue"); + nodeService.setProperty(record_1, RecordsManagementModel.PROP_PUBLICATION_DATE, new Date()); + nodeService.setProperty(record_1, ContentModel.PROP_TITLE, "titleValue"); + recordsManagementActionService.executeRecordsManagementAction(record_1, "declareRecord"); + + nodeService.setProperty(record_2, RecordsManagementModel.PROP_ORIGINATOR, "origValue"); + nodeService.setProperty(record_2, RecordsManagementModel.PROP_ORIGINATING_ORGANIZATION, "origOrgValue"); + nodeService.setProperty(record_2, RecordsManagementModel.PROP_PUBLICATION_DATE, new Date()); + nodeService.setProperty(record_2, ContentModel.PROP_TITLE, "titleValue"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "declareRecord"); + + NodeRef ndNodeRef = this.nodeService.getChildAssocs(recordFolder_1, RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + this.nodeService.setProperty(ndNodeRef, RecordsManagementModel.PROP_DISPOSITION_AS_OF, calendar.getTime()); + ndNodeRef = this.nodeService.getChildAssocs(record_2, RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + this.nodeService.setProperty(ndNodeRef, RecordsManagementModel.PROP_DISPOSITION_AS_OF, calendar.getTime()); + + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "cutoff", null); + recordsManagementActionService.executeRecordsManagementAction(record_2, "cutoff", null); + + ndNodeRef = this.nodeService.getChildAssocs(recordFolder_1, RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + this.nodeService.setProperty(ndNodeRef, RecordsManagementModel.PROP_DISPOSITION_AS_OF, calendar.getTime()); + ndNodeRef = this.nodeService.getChildAssocs(record_2, RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + this.nodeService.setProperty(ndNodeRef, RecordsManagementModel.PROP_DISPOSITION_AS_OF, calendar.getTime()); + + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "transfer", null); + recordsManagementActionService.executeRecordsManagementAction(record_2, "transfer", null); + recordsManagementActionService.executeRecordsManagementAction(getTransferObject(recordFolder_1), "transferComplete", null); + + assertTrue(this.nodeService.exists(recordFolder_1)); + ndNodeRef = this.nodeService.getChildAssocs(recordFolder_1, RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + this.nodeService.setProperty(ndNodeRef, RecordsManagementModel.PROP_DISPOSITION_AS_OF, calendar.getTime()); + assertTrue(this.nodeService.exists(recordFolder_1)); + ndNodeRef = this.nodeService.getChildAssocs(record_2, RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + this.nodeService.setProperty(ndNodeRef, RecordsManagementModel.PROP_DISPOSITION_AS_OF, calendar.getTime()); + + // folder level + + assertTrue(this.nodeService.exists(recordFolder_1)); + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.ALLOWED); + checkCapability(rm_administrator, recordFolder_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, recordFolder_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, recordFolder_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_administrator, record_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_records_manager, record_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_security_officer, record_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_power_user, record_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_user, record_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + + // record level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_administrator, recordFolder_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_records_manager, recordFolder_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_security_officer, recordFolder_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.ALLOWED); + checkCapability(rm_administrator, record_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, record_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, record_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_power_user, record_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(rm_user, record_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + + // check person with no access and add read and write + // Filing + + checkCapability(test_user, recordFolder_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS, true); + permissionService.setInheritParentPermissions(recordCategory_1, false); + permissionService.setInheritParentPermissions(recordCategory_2, false); + permissionService.setPermission(recordCategory_1, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordCategory_2, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.DECLARE_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.ALLOWED); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.DECLARE_RECORDS); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.ALLOWED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.DECLARE_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.ALLOWED); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.ALLOWED); + + permissionService.deletePermission(recordFolder_1, testers, RMPermissionModel.FILING); + permissionService.deletePermission(recordFolder_2, testers, RMPermissionModel.FILING); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.ALLOWED); + + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.ALLOWED); + + // check frozen + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + Map params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "one"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "freeze", params); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.ALLOWED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "Two"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "freeze", params); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "unfreeze"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "unfreeze"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.ALLOWED); + + // Check closed + // should make no difference + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "closeRecordFolder"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "closeRecordFolder"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.ALLOWED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "openRecordFolder"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "openRecordFolder"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.ALLOWED); + } + + private void setupForAccessionComplete() + { + checkCapability(test_user, recordFolder_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.ALLOWED); + + // check each action + + TransferAction transfer = (TransferAction) ctx.getBean("accession"); + assertFalse(transfer.isExecutable(recordFolder_1, null)); + assertFalse(transfer.isExecutable(record_1, null)); + assertFalse(transfer.isExecutable(recordFolder_2, null)); + assertFalse(transfer.isExecutable(record_2, null)); + + TransferCompleteAction transferComplete = (TransferCompleteAction) ctx.getBean("accessionComplete"); + assertTrue(transferComplete.isExecutable(recordFolder_1, null)); + assertFalse(transferComplete.isExecutable(record_1, null)); + assertFalse(transferComplete.isExecutable(recordFolder_2, null)); + assertTrue(transferComplete.isExecutable(record_2, null)); + } + + public void testAuthorizeNominatedTransfersCapability() + { + setupForAccession(); + + // try accession + + AuthenticationUtil.setFullyAuthenticatedUser(test_user); + + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "accession", null); + + recordsManagementActionService.executeRecordsManagementAction(record_2, "accession", null); + + setupForAccessionComplete(); + + // try and complete the transfer + + AuthenticationUtil.setFullyAuthenticatedUser(test_user); + recordsManagementActionService.executeRecordsManagementAction(getTransferObject(recordFolder_1), "accessionComplete", null); + } + + public void testAuthorizeNominatedTransfersCapability_AccessionNegative() + { + setupForAccession(); + + // try accession + + AuthenticationUtil.setFullyAuthenticatedUser(test_user); + + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "accession", null); + + recordsManagementActionService.executeRecordsManagementAction(record_2, "accession", null); + + // -ve checks (ALF-2749) + // note: ideally, each -ve test should be run independently (if we want outer/setup txn to rollback) + + try + { + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "accession", null); + fail(); + } + catch (AccessDeniedException ade) + { + + } + try + { + recordsManagementActionService.executeRecordsManagementAction(record_1, "accession", null); + fail(); + } + catch (AccessDeniedException ade) + { + + } + + // check protected properties + + // PROP_DISPOSITION_ACTION_STARTED_AT + // PROP_DISPOSITION_ACTION_STARTED_BY + // PROP_DISPOSITION_ACTION_COMPLETED_AT + // PROP_DISPOSITION_ACTION_COMPLETED_BY + + try + { + publicNodeService.setProperty(record_1, RecordsManagementModel.PROP_DISPOSITION_ACTION_STARTED_AT, true); + fail(); + } + catch (AccessDeniedException ade) + { + + } + + try + { + publicNodeService.setProperty(record_1, RecordsManagementModel.PROP_DISPOSITION_ACTION_STARTED_BY, true); + fail(); + } + catch (AccessDeniedException ade) + { + + } + + try + { + publicNodeService.setProperty(record_1, RecordsManagementModel.PROP_DISPOSITION_ACTION_COMPLETED_AT, true); + fail(); + } + catch (AccessDeniedException ade) + { + + } + + try + { + publicNodeService.setProperty(record_1, RecordsManagementModel.PROP_DISPOSITION_ACTION_COMPLETED_BY, true); + fail(); + } + catch (AccessDeniedException ade) + { + + } + + // check cutoff again (it is already cut off) + + try + { + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "accession", null); + fail(); + } + catch (AccessDeniedException ade) + { + + } + catch (AlfrescoRuntimeException are) + { + + } + try + { + recordsManagementActionService.executeRecordsManagementAction(record_2, "accession", null); + fail(); + } + catch (AccessDeniedException ade) + { + + } + catch (AlfrescoRuntimeException are) + { + + } + } + + public void testAuthorizeNominatedTransfersCapability_AccessionCompleteNegative() + { + setupForAccession(); + + // try accession + + AuthenticationUtil.setFullyAuthenticatedUser(test_user); + + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "accession", null); + + recordsManagementActionService.executeRecordsManagementAction(record_2, "accession", null); + + setupForAccessionComplete(); + + // try and complete the transfer + + AuthenticationUtil.setFullyAuthenticatedUser(test_user); + recordsManagementActionService.executeRecordsManagementAction(getTransferObject(recordFolder_1), "accessionComplete", null); + + // -ve checks (ALF-2749) + // note: ideally, each -ve test should be run independently (if we want outer/setup txn to rollback) + + try + { + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "accessionComplete", null); + fail(); + } + catch (AccessDeniedException ade) + { + + } + catch (AlfrescoRuntimeException are) + { + + } + try + { + recordsManagementActionService.executeRecordsManagementAction(record_1, "accessionComplete", null); + fail(); + } + catch (AccessDeniedException ade) + { + + } + catch (AlfrescoRuntimeException are) + { + + } + try + { + // will fail as this is in the same transfer which is now done. + recordsManagementActionService.executeRecordsManagementAction(getTransferObject(record_2), "accessionComplete", null); + fail(); + } + catch (AccessDeniedException ade) + { + + } + catch (AlfrescoRuntimeException are) + { + + } + + // try again - should fail + + try + { + recordsManagementActionService.executeRecordsManagementAction(getTransferObject(recordFolder_1), "accessionComplete", null); + fail(); + } + catch (AccessDeniedException ade) + { + + } + catch (AlfrescoRuntimeException are) + { + + } + try + { + recordsManagementActionService.executeRecordsManagementAction(getTransferObject(record_2), "accessionComplete", null); + fail(); + } + catch (AccessDeniedException ade) + { + + } + catch (AlfrescoRuntimeException are) + { + + } + } + + public void testChangeOrDeleteReferencesCapability() + { + // capability is checked above - just check permission assignments + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.CHANGE_OR_DELETE_REFERENCES, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.CHANGE_OR_DELETE_REFERENCES, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.CHANGE_OR_DELETE_REFERENCES, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.CHANGE_OR_DELETE_REFERENCES, AccessStatus.DENIED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.CHANGE_OR_DELETE_REFERENCES, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.CHANGE_OR_DELETE_REFERENCES, AccessStatus.DENIED); + } + + public void testCloseFoldersCapability() + { + // Folder + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + checkPermission(rm_user, filePlan, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + + // folder level - no preconditions + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_administrator, recordFolder_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, recordFolder_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, recordFolder_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_power_user, recordFolder_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_user, recordFolder_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_administrator, record_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_records_manager, record_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_security_officer, record_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_power_user, record_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_user, record_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + + // record level - record denies - folder allows + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_administrator, recordFolder_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, recordFolder_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, recordFolder_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_power_user, recordFolder_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_user, recordFolder_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_administrator, record_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_records_manager, record_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_security_officer, record_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_power_user, record_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_user, record_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + + // Set appropriate state - declare records and make eligible for cut off + + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.HOUR, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + + nodeService.setProperty(record_1, RecordsManagementModel.PROP_ORIGINATOR, "origValue"); + nodeService.setProperty(record_1, RecordsManagementModel.PROP_ORIGINATING_ORGANIZATION, "origOrgValue"); + nodeService.setProperty(record_1, RecordsManagementModel.PROP_PUBLICATION_DATE, new Date()); + nodeService.setProperty(record_1, ContentModel.PROP_TITLE, "titleValue"); + recordsManagementActionService.executeRecordsManagementAction(record_1, "declareRecord"); + + nodeService.setProperty(record_2, RecordsManagementModel.PROP_ORIGINATOR, "origValue"); + nodeService.setProperty(record_2, RecordsManagementModel.PROP_ORIGINATING_ORGANIZATION, "origOrgValue"); + nodeService.setProperty(record_2, RecordsManagementModel.PROP_PUBLICATION_DATE, new Date()); + nodeService.setProperty(record_2, ContentModel.PROP_TITLE, "titleValue"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "declareRecord"); + + NodeRef ndNodeRef = this.nodeService.getChildAssocs(recordFolder_1, RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + this.nodeService.setProperty(ndNodeRef, RecordsManagementModel.PROP_DISPOSITION_AS_OF, calendar.getTime()); + ndNodeRef = this.nodeService.getChildAssocs(record_2, RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + this.nodeService.setProperty(ndNodeRef, RecordsManagementModel.PROP_DISPOSITION_AS_OF, calendar.getTime()); + + // folder level + + assertTrue(this.nodeService.exists(recordFolder_1)); + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_administrator, recordFolder_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, recordFolder_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, recordFolder_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_power_user, recordFolder_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_user, recordFolder_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_administrator, record_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_records_manager, record_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_security_officer, record_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_power_user, record_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_user, record_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + + // record level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_administrator, recordFolder_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, recordFolder_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, recordFolder_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_power_user, recordFolder_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_user, recordFolder_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_administrator, record_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_records_manager, record_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_security_officer, record_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_power_user, record_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_user, record_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + + // check person with no access and add read and write + // Filing + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS, true); + permissionService.setInheritParentPermissions(recordCategory_1, false); + permissionService.setInheritParentPermissions(recordCategory_2, false); + permissionService.setPermission(recordCategory_1, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordCategory_2, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.DECLARE_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.CLOSE_FOLDERS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.DECLARE_RECORDS); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.DECLARE_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + + permissionService.deletePermission(recordFolder_1, testers, RMPermissionModel.FILING); + permissionService.deletePermission(recordFolder_2, testers, RMPermissionModel.FILING); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + + // check frozen + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + Map params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "one"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "freeze", params); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "Two"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "freeze", params); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "Two"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "freeze", params); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "unfreeze"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "unfreeze"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "unfreeze"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + + // Check closed + // should make no difference + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "closeRecordFolder"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "closeRecordFolder"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "openRecordFolder"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "openRecordFolder"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.CLOSE_FOLDERS, AccessStatus.DENIED); + + // try to close + + AuthenticationUtil.setFullyAuthenticatedUser(test_user); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "closeRecordFolder", null); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "closeRecordFolder", null); + + try + { + recordsManagementActionService.executeRecordsManagementAction(record_1, "closeRecordFolder", null); + fail(); + } + catch (AccessDeniedException ade) + { + + } + try + { + recordsManagementActionService.executeRecordsManagementAction(record_2, "closeRecordFolder", null); + fail(); + } + catch (AccessDeniedException ade) + { + + } + + // check protected properties + + // PROP_IS_CLOSED + + try + { + publicNodeService.setProperty(record_1, RecordsManagementModel.PROP_IS_CLOSED, true); + fail(); + } + catch (AccessDeniedException ade) + { + + } + + // check close again (it is already closed) + + AuthenticationUtil.setFullyAuthenticatedUser(test_user); + try + { + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "closeRecordFolder", null); + fail(); + } + catch (AccessDeniedException ade) + { + + } + try + { + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "closeRecordFolder", null); + fail(); + } + catch (AccessDeniedException ade) + { + + } + try + { + recordsManagementActionService.executeRecordsManagementAction(record_1, "closeRecordFolder", null); + fail(); + } + catch (AccessDeniedException ade) + { + + } + try + { + recordsManagementActionService.executeRecordsManagementAction(record_2, "closeRecordFolder", null); + fail(); + } + catch (AccessDeniedException ade) + { + + } + } + + public void testCreateAndAssociateSelectionListsCapability() + { + // capability is checked above - just check permission assignments + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS, AccessStatus.DENIED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.CREATE_AND_ASSOCIATE_SELECTION_LISTS, AccessStatus.DENIED); + } + + public void testCreateModifyDestroyClassificationGuidesCapability() + { + // capability is checked above - just check permission assignments + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, AccessStatus.ALLOWED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_CLASSIFICATION_GUIDES, AccessStatus.DENIED); + } + + public void testCreateModifyDestroyEventsCapability() + { + // capability is checked above - just check permission assignments + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS, AccessStatus.DENIED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_EVENTS, AccessStatus.DENIED); + } + + public void testCreateModifyDestroyFileplanMetadataCapability() + { + // capability is checked above - just check permission assignments + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, AccessStatus.DENIED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, AccessStatus.DENIED); + } + + public void testCreateModifyDestroyFileplanTypesCapability() + { + // capability is checked above - just check permission assignments + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.DENIED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.DENIED); + } + + public void testCreateModifyDestroyFoldersCapability() + { + // Folder + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + checkPermission(rm_user, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + + // folder level - no preconditions + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_administrator, recordFolder_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, recordFolder_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, recordFolder_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_power_user, recordFolder_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_user, recordFolder_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_administrator, record_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_records_manager, record_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_security_officer, record_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_power_user, record_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_user, record_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + + // record level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_administrator, recordFolder_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, recordFolder_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, recordFolder_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_power_user, recordFolder_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_user, recordFolder_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_administrator, record_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_records_manager, record_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_security_officer, record_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_power_user, record_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_user, record_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + + // check person with no access and add read and write + // Filing + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS, true); + permissionService.setInheritParentPermissions(recordCategory_1, false); + permissionService.setInheritParentPermissions(recordCategory_2, false); + permissionService.setPermission(recordCategory_1, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordCategory_2, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.DECLARE_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.DECLARE_RECORDS); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.DECLARE_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + + permissionService.deletePermission(recordFolder_1, testers, RMPermissionModel.FILING); + permissionService.deletePermission(recordFolder_2, testers, RMPermissionModel.FILING); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + + // check frozen + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + Map params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "one"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "freeze", params); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "Two"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "freeze", params); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "Two"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "freeze", params); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "unfreeze"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "unfreeze"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "unfreeze"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + + // Check closed + // should make no difference + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "closeRecordFolder"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "closeRecordFolder"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "openRecordFolder"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "openRecordFolder"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + + // series level capabilities + + // fails as no filling rights ... + + checkCapability(test_user, recordCategory_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordCategory_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.DENIED); + + permissionService.setPermission(recordCategory_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordCategory_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, recordCategory_1, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, recordCategory_2, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS, AccessStatus.ALLOWED); + + // create + + HashMap properties = new HashMap(); + properties.put(ContentModel.PROP_NAME, "name"); + properties.put(PROP_IDENTIFIER, "identifier"); + properties.put(ContentModel.PROP_TITLE, "title"); + properties.put(ContentModel.PROP_DESCRIPTION, "description"); + properties.put(PROP_REVIEW_PERIOD, "week|1"); + properties.put(PROP_VITAL_RECORD_INDICATOR, true); + NodeRef newFolder = publicNodeService.createNode(recordCategory_1, ContentModel.ASSOC_CONTAINS, TYPE_RECORD_FOLDER, TYPE_RECORD_FOLDER, + properties).getChildRef(); + + // modify + + publicNodeService.addAspect(newFolder, ContentModel.ASPECT_OWNABLE, null); + properties = new HashMap(); + properties.put(ContentModel.PROP_OWNER, "me"); + publicNodeService.addProperties(newFolder, properties); + // move should fail ... + try + { + publicNodeService.moveNode(newFolder, recordCategory_2, ContentModel.ASSOC_CONTAINS, TYPE_RECORD_FOLDER); + fail(); + } + catch (AccessDeniedException ade) + { + + } + publicNodeService.removeProperty(newFolder, ContentModel.PROP_TITLE); + publicNodeService.setProperty(newFolder, ContentModel.PROP_TITLE, "title"); + publicNodeService.addAspect(newFolder, ContentModel.ASPECT_TEMPORARY, null); + publicNodeService.removeAspect(newFolder, ContentModel.ASPECT_TEMPORARY); + publicNodeService.setProperties(newFolder, publicNodeService.getProperties(newFolder)); + try + { + // abstains + publicNodeService.setType(newFolder, TYPE_RECORD_FOLDER); + fail(); + } + catch (AccessDeniedException ade) + { + + } + + // try move + + permissionService.setPermission(filePlan, testers, RMPermissionModel.MOVE_RECORDS, true); + publicNodeService.moveNode(newFolder, recordCategory_2, ContentModel.ASSOC_CONTAINS, TYPE_RECORD_FOLDER); + + // delete + + publicNodeService.deleteNode(newFolder); + publicNodeService.deleteNode(recordFolder_1); + publicNodeService.deleteNode(recordFolder_2); + + } + + public void testCreateModifyDestroyRecordTypesCapability() + { + // capability is checked above - just check permission assignments + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.DENIED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.DENIED); + } + + public void testCreateModifyDestroyReferenceTypesCapability() + { + // capability is checked above - just check permission assignments + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES, AccessStatus.DENIED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_REFERENCE_TYPES, AccessStatus.DENIED); + } + + public void testCreateModifyDestroyRolesCapability() + { + // capability is checked above - just check permission assignments + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES, AccessStatus.DENIED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_ROLES, AccessStatus.DENIED); + } + + public void testCreateModifyDestroyTimeframesCapability() + { + // capability is checked above - just check permission assignments + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES, AccessStatus.DENIED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_TIMEFRAMES, AccessStatus.DENIED); + } + + public void testCreateModifyDestroyUsersAndGroupsCapability() + { + // capability is checked above - just check permission assignments + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, AccessStatus.DENIED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_USERS_AND_GROUPS, AccessStatus.DENIED); + } + + public void testCreateModifyRecordsInCuttoffFoldersCapability() + { + // Folder + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + + // folder level - no preconditions + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_administrator, recordFolder_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, recordFolder_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, recordFolder_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_administrator, record_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, record_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, record_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_power_user, record_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_user, record_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + + // record level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_administrator, recordFolder_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, recordFolder_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, recordFolder_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_administrator, record_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, record_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, record_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_power_user, record_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_user, record_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + + // check person with no access and add read and write + // Filing + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS, true); + permissionService.setInheritParentPermissions(recordCategory_1, false); + permissionService.setInheritParentPermissions(recordCategory_2, false); + permissionService.setPermission(recordCategory_1, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordCategory_2, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.DECLARE_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.DECLARE_RECORDS); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.DECLARE_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + + permissionService.deletePermission(recordFolder_1, testers, RMPermissionModel.FILING); + permissionService.deletePermission(recordFolder_2, testers, RMPermissionModel.FILING); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + + // check frozen + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + Map params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "one"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "freeze", params); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "Two"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "freeze", params); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "Two"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "freeze", params); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.DENIED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "unfreeze"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "unfreeze"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "unfreeze"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + + // Check closed + // should make no difference + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "closeRecordFolder"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "closeRecordFolder"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "openRecordFolder"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "openRecordFolder"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + + // Check cutoff + // should make no difference + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.HOUR, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + + nodeService.setProperty(record_1, RecordsManagementModel.PROP_ORIGINATOR, "origValue"); + nodeService.setProperty(record_1, RecordsManagementModel.PROP_ORIGINATING_ORGANIZATION, "origOrgValue"); + nodeService.setProperty(record_1, RecordsManagementModel.PROP_PUBLICATION_DATE, new Date()); + nodeService.setProperty(record_1, ContentModel.PROP_TITLE, "titleValue"); + recordsManagementActionService.executeRecordsManagementAction(record_1, "declareRecord"); + + nodeService.setProperty(record_2, RecordsManagementModel.PROP_ORIGINATOR, "origValue"); + nodeService.setProperty(record_2, RecordsManagementModel.PROP_ORIGINATING_ORGANIZATION, "origOrgValue"); + nodeService.setProperty(record_2, RecordsManagementModel.PROP_PUBLICATION_DATE, new Date()); + nodeService.setProperty(record_2, ContentModel.PROP_TITLE, "titleValue"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "declareRecord"); + + NodeRef ndNodeRef = this.nodeService.getChildAssocs(recordFolder_1, RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + this.nodeService.setProperty(ndNodeRef, RecordsManagementModel.PROP_DISPOSITION_AS_OF, calendar.getTime()); + ndNodeRef = this.nodeService.getChildAssocs(record_2, RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + this.nodeService.setProperty(ndNodeRef, RecordsManagementModel.PROP_DISPOSITION_AS_OF, calendar.getTime()); + + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "cutoff"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "cutoff"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS, AccessStatus.ALLOWED); + + // create + + Map properties = new HashMap(1); + properties.put(ContentModel.PROP_NAME, "MyRecordCreate.txt"); + NodeRef newRecord = this.publicNodeService.createNode(recordFolder_1, ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "MyRecord.txt"), ContentModel.TYPE_CONTENT, properties).getChildRef(); + + // Set the content + ContentWriter writer = this.publicContentService.getWriter(newRecord, ContentModel.PROP_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.setEncoding("UTF-8"); + writer.putContent("There is some content in this record"); + + recordsManagementActionService.executeRecordsManagementAction(newRecord, "file"); + // modify + + publicNodeService.addAspect(newRecord, ContentModel.ASPECT_OWNABLE, null); + properties = new HashMap(); + properties.put(ContentModel.PROP_OWNER, "me"); + publicNodeService.addProperties(newRecord, properties); + // move should fail ... + try + { + publicNodeService.moveNode(newRecord, recordCategory_2, ContentModel.ASSOC_CONTAINS, TYPE_RECORD_FOLDER); + fail(); + } + catch (AccessDeniedException ade) + { + + } + publicNodeService.removeProperty(newRecord, ContentModel.PROP_TITLE); + publicNodeService.setProperty(newRecord, ContentModel.PROP_TITLE, "title"); + publicNodeService.addAspect(newRecord, ContentModel.ASPECT_TEMPORARY, null); + publicNodeService.removeAspect(newRecord, ContentModel.ASPECT_TEMPORARY); + publicNodeService.setProperties(newRecord, publicNodeService.getProperties(newRecord)); + try + { + // abstains + publicNodeService.setType(newRecord, TYPE_RECORD_FOLDER); + fail(); + } + catch (AccessDeniedException ade) + { + + } + + } + + public void testCycleVitalRecordsCapability() + { + // Folder + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkPermission(rm_user, filePlan, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + + // folder level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_administrator, recordFolder_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, recordFolder_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, recordFolder_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_power_user, recordFolder_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_user, recordFolder_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_administrator, record_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, record_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, record_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_power_user, record_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_user, record_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + + // record level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_administrator, recordFolder_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, recordFolder_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, recordFolder_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_power_user, recordFolder_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_user, recordFolder_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_administrator, record_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, record_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, record_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_power_user, record_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_user, record_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + + // check person with no access and add read and write + // Filing + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS, true); + permissionService.setInheritParentPermissions(recordCategory_1, false); + permissionService.setInheritParentPermissions(recordCategory_2, false); + permissionService.setPermission(recordCategory_1, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordCategory_2, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.DECLARE_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.CYCLE_VITAL_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.DECLARE_RECORDS); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.DECLARE_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + + permissionService.deletePermission(recordFolder_1, testers, RMPermissionModel.FILING); + permissionService.deletePermission(recordFolder_2, testers, RMPermissionModel.FILING); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + + // check frozen + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + Map params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "one"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "freeze", params); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "Two"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "freeze", params); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.DENIED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "unfreeze"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "unfreeze"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + + // Check closed + // should make no difference + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "closeRecordFolder"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "closeRecordFolder"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "openRecordFolder"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "openRecordFolder"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + + // try and cycle + + recordsManagementActionService.executeRecordsManagementAction(record_1, "reviewed"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "reviewed"); + + recordsManagementActionService.executeRecordsManagementAction(record_1, "reviewed"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "reviewed"); + + // check cutoff + + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.HOUR, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + + nodeService.setProperty(record_1, RecordsManagementModel.PROP_ORIGINATOR, "origValue"); + nodeService.setProperty(record_1, RecordsManagementModel.PROP_ORIGINATING_ORGANIZATION, "origOrgValue"); + nodeService.setProperty(record_1, RecordsManagementModel.PROP_PUBLICATION_DATE, new Date()); + nodeService.setProperty(record_1, ContentModel.PROP_TITLE, "titleValue"); + recordsManagementActionService.executeRecordsManagementAction(record_1, "declareRecord"); + + nodeService.setProperty(record_2, RecordsManagementModel.PROP_ORIGINATOR, "origValue"); + nodeService.setProperty(record_2, RecordsManagementModel.PROP_ORIGINATING_ORGANIZATION, "origOrgValue"); + nodeService.setProperty(record_2, RecordsManagementModel.PROP_PUBLICATION_DATE, new Date()); + nodeService.setProperty(record_2, ContentModel.PROP_TITLE, "titleValue"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "declareRecord"); + + NodeRef ndNodeRef = this.nodeService.getChildAssocs(recordFolder_1, RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + this.nodeService.setProperty(ndNodeRef, RecordsManagementModel.PROP_DISPOSITION_AS_OF, calendar.getTime()); + ndNodeRef = this.nodeService.getChildAssocs(record_2, RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + this.nodeService.setProperty(ndNodeRef, RecordsManagementModel.PROP_DISPOSITION_AS_OF, calendar.getTime()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "cutoff", null); + recordsManagementActionService.executeRecordsManagementAction(record_2, "cutoff", null); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.CYCLE_VITAL_RECORDS, AccessStatus.ALLOWED); + } + + public void testDeclareAuditAsRecordCapability() + { + // capability is checked above - just check permission assignments + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.DECLARE_AUDIT_AS_RECORD, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.DECLARE_AUDIT_AS_RECORD, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.DECLARE_AUDIT_AS_RECORD, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.DECLARE_AUDIT_AS_RECORD, AccessStatus.DENIED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.DECLARE_AUDIT_AS_RECORD, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.DECLARE_AUDIT_AS_RECORD, AccessStatus.DENIED); + } + + public void testDeclareRecordsCapability() + { + // Folder + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.DECLARE_RECORDS, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.DECLARE_RECORDS, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.DECLARE_RECORDS, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.DECLARE_RECORDS, AccessStatus.ALLOWED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.DECLARE_RECORDS, AccessStatus.ALLOWED); + checkPermission(rm_user, filePlan, RMPermissionModel.DECLARE_RECORDS, AccessStatus.ALLOWED); + + // folder level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_administrator, recordFolder_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_records_manager, recordFolder_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_security_officer, recordFolder_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_administrator, record_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_records_manager, record_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_security_officer, record_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_power_user, record_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_user, record_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + + // record level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_administrator, recordFolder_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_records_manager, recordFolder_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_security_officer, recordFolder_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_administrator, record_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_records_manager, record_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_security_officer, record_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_power_user, record_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_user, record_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + + // Set appropriate state - declare records and make eligible + + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.HOUR, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + + nodeService.setProperty(record_1, RecordsManagementModel.PROP_ORIGINATOR, "origValue"); + nodeService.setProperty(record_1, RecordsManagementModel.PROP_ORIGINATING_ORGANIZATION, "origOrgValue"); + nodeService.setProperty(record_1, RecordsManagementModel.PROP_PUBLICATION_DATE, new Date()); + nodeService.setProperty(record_1, ContentModel.PROP_TITLE, "titleValue"); + // recordsManagementActionService.executeRecordsManagementAction(record_1, "declareRecord"); + + nodeService.setProperty(record_2, RecordsManagementModel.PROP_ORIGINATOR, "origValue"); + nodeService.setProperty(record_2, RecordsManagementModel.PROP_ORIGINATING_ORGANIZATION, "origOrgValue"); + nodeService.setProperty(record_2, RecordsManagementModel.PROP_PUBLICATION_DATE, new Date()); + nodeService.setProperty(record_2, ContentModel.PROP_TITLE, "titleValue"); + // recordsManagementActionService.executeRecordsManagementAction(record_2, "declareRecord"); + + // folder level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_administrator, recordFolder_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_records_manager, recordFolder_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_security_officer, recordFolder_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_administrator, record_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, record_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, record_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_power_user, record_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_user, record_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.ALLOWED); + + // record level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_administrator, recordFolder_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_records_manager, recordFolder_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_security_officer, recordFolder_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_administrator, record_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, record_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, record_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_power_user, record_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_user, record_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.ALLOWED); + + // check person with no access and add read and write + // Filing + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS, true); + permissionService.setInheritParentPermissions(recordCategory_1, false); + permissionService.setInheritParentPermissions(recordCategory_2, false); + permissionService.setPermission(recordCategory_1, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordCategory_2, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.DECLARE_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.ALLOWED); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.DECLARE_RECORDS); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.DECLARE_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.ALLOWED); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.ALLOWED); + + permissionService.deletePermission(recordFolder_1, testers, RMPermissionModel.FILING); + permissionService.deletePermission(recordFolder_2, testers, RMPermissionModel.FILING); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.ALLOWED); + + // check frozen + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + Map params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "one"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "freeze", params); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.ALLOWED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "Two"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "freeze", params); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "unfreeze"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "unfreeze"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.ALLOWED); + + // Check closed + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "closeRecordFolder"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "closeRecordFolder"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "openRecordFolder"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "openRecordFolder"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.ALLOWED); + + // try declare + + AuthenticationUtil.setFullyAuthenticatedUser(test_user); + try + { + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "declareRecord", null); + fail(); + } + catch (AccessDeniedException ade) + { + + } + recordsManagementActionService.executeRecordsManagementAction(record_1, "declareRecord"); + try + { + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "declareRecord", null); + fail(); + } + catch (AccessDeniedException ade) + { + + } + recordsManagementActionService.executeRecordsManagementAction(record_2, "declareRecord"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DECLARE_RECORDS, AccessStatus.DENIED); + } + + public void testDeclareRecordsInClosedFoldersCapability() + { + // Folder + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.ALLOWED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.ALLOWED); + checkPermission(rm_user, filePlan, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + + // folder level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_administrator, recordFolder_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_records_manager, recordFolder_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_security_officer, recordFolder_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_administrator, record_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_records_manager, record_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_security_officer, record_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_power_user, record_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_user, record_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + + // record level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_administrator, recordFolder_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_records_manager, recordFolder_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_security_officer, recordFolder_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_administrator, record_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_records_manager, record_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_security_officer, record_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_power_user, record_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_user, record_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + + // Set appropriate state - declare records and make eligible + + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.HOUR, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + + nodeService.setProperty(record_1, RecordsManagementModel.PROP_ORIGINATOR, "origValue"); + nodeService.setProperty(record_1, RecordsManagementModel.PROP_ORIGINATING_ORGANIZATION, "origOrgValue"); + nodeService.setProperty(record_1, RecordsManagementModel.PROP_PUBLICATION_DATE, new Date()); + nodeService.setProperty(record_1, ContentModel.PROP_TITLE, "titleValue"); + // recordsManagementActionService.executeRecordsManagementAction(record_1, "declareRecord"); + + nodeService.setProperty(record_2, RecordsManagementModel.PROP_ORIGINATOR, "origValue"); + nodeService.setProperty(record_2, RecordsManagementModel.PROP_ORIGINATING_ORGANIZATION, "origOrgValue"); + nodeService.setProperty(record_2, RecordsManagementModel.PROP_PUBLICATION_DATE, new Date()); + nodeService.setProperty(record_2, ContentModel.PROP_TITLE, "titleValue"); + // recordsManagementActionService.executeRecordsManagementAction(record_2, "declareRecord"); + + // folder level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_administrator, recordFolder_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_records_manager, recordFolder_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_security_officer, recordFolder_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_administrator, record_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, record_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, record_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_power_user, record_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_user, record_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + + // record level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_administrator, recordFolder_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_records_manager, recordFolder_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_security_officer, recordFolder_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_administrator, record_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, record_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, record_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_power_user, record_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_user, record_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + + // check person with no access and add read and write + // Filing + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS, true); + permissionService.setInheritParentPermissions(recordCategory_1, false); + permissionService.setInheritParentPermissions(recordCategory_2, false); + permissionService.setPermission(recordCategory_1, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordCategory_2, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.ALLOWED); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.ALLOWED); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.ALLOWED); + + permissionService.deletePermission(recordFolder_1, testers, RMPermissionModel.FILING); + permissionService.deletePermission(recordFolder_2, testers, RMPermissionModel.FILING); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.ALLOWED); + + // check frozen + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + Map params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "one"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "freeze", params); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.ALLOWED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "Two"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "freeze", params); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "unfreeze"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "unfreeze"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.ALLOWED); + + // Check closed + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "closeRecordFolder"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "closeRecordFolder"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.ALLOWED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "openRecordFolder"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "openRecordFolder"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.ALLOWED); + + // try declare in closed + + // Close + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "closeRecordFolder"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "closeRecordFolder"); + + AuthenticationUtil.setFullyAuthenticatedUser(test_user); + try + { + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "declareRecord", null); + fail(); + } + catch (AccessDeniedException ade) + { + + } + recordsManagementActionService.executeRecordsManagementAction(record_1, "declareRecord"); + try + { + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "declareRecord", null); + fail(); + } + catch (AccessDeniedException ade) + { + + } + recordsManagementActionService.executeRecordsManagementAction(record_2, "declareRecord"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS, AccessStatus.DENIED); + } + + public void testDeleteAuditCapability() + { + // capability is checked above - just check permission assignments + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.DELETE_AUDIT, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.DELETE_AUDIT, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.DELETE_AUDIT, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.DELETE_AUDIT, AccessStatus.DENIED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.DELETE_AUDIT, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.DELETE_AUDIT, AccessStatus.DENIED); + } + + public void testDeleteLinksCapability() + { + // capability is checked above - just check permission assignments + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.DELETE_LINKS, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.DELETE_LINKS, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.DELETE_LINKS, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.DELETE_LINKS, AccessStatus.DENIED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.DELETE_LINKS, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.DELETE_LINKS, AccessStatus.DENIED); + } + + public void testDeleteRecordsCapability() + { + // capability is checked above - just check permission assignments + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.DELETE_RECORDS, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.DELETE_RECORDS, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.DELETE_RECORDS, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.DELETE_RECORDS, AccessStatus.DENIED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.DELETE_RECORDS, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.DELETE_RECORDS, AccessStatus.DENIED); + } + + public void testDestroyRecordsCapability() + { + // Folder + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.DESTROY_RECORDS, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.DESTROY_RECORDS, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.DESTROY_RECORDS, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + + // folder level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_administrator, recordFolder_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, recordFolder_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, recordFolder_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(rm_administrator, record_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(rm_records_manager, record_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(rm_security_officer, record_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(rm_power_user, record_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(rm_user, record_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + + // record level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(rm_administrator, recordFolder_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(rm_records_manager, recordFolder_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(rm_security_officer, recordFolder_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_administrator, record_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, record_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, record_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(rm_power_user, record_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(rm_user, record_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + + // Set appropriate state - declare records and make eligible + + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.HOUR, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + + nodeService.setProperty(record_1, RecordsManagementModel.PROP_ORIGINATOR, "origValue"); + nodeService.setProperty(record_1, RecordsManagementModel.PROP_ORIGINATING_ORGANIZATION, "origOrgValue"); + nodeService.setProperty(record_1, RecordsManagementModel.PROP_PUBLICATION_DATE, new Date()); + nodeService.setProperty(record_1, ContentModel.PROP_TITLE, "titleValue"); + recordsManagementActionService.executeRecordsManagementAction(record_1, "declareRecord"); + + nodeService.setProperty(record_2, RecordsManagementModel.PROP_ORIGINATOR, "origValue"); + nodeService.setProperty(record_2, RecordsManagementModel.PROP_ORIGINATING_ORGANIZATION, "origOrgValue"); + nodeService.setProperty(record_2, RecordsManagementModel.PROP_PUBLICATION_DATE, new Date()); + nodeService.setProperty(record_2, ContentModel.PROP_TITLE, "titleValue"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "declareRecord"); + + NodeRef ndNodeRef = this.nodeService.getChildAssocs(recordFolder_1, RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + this.nodeService.setProperty(ndNodeRef, RecordsManagementModel.PROP_DISPOSITION_AS_OF, calendar.getTime()); + ndNodeRef = this.nodeService.getChildAssocs(record_2, RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + this.nodeService.setProperty(ndNodeRef, RecordsManagementModel.PROP_DISPOSITION_AS_OF, calendar.getTime()); + + // folder level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_administrator, recordFolder_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, recordFolder_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, recordFolder_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(rm_administrator, record_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(rm_records_manager, record_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(rm_security_officer, record_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(rm_power_user, record_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(rm_user, record_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + + // record level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(rm_administrator, recordFolder_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(rm_records_manager, recordFolder_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(rm_security_officer, recordFolder_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_administrator, record_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, record_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, record_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(rm_power_user, record_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(rm_user, record_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + + // check person with no access and add read and write + // Filing + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS, true); + permissionService.setInheritParentPermissions(recordCategory_1, false); + permissionService.setInheritParentPermissions(recordCategory_2, false); + permissionService.setPermission(recordCategory_1, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordCategory_2, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.DECLARE_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.DESTROY_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.ALLOWED); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.DECLARE_RECORDS); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.ALLOWED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.DECLARE_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.ALLOWED); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.ALLOWED); + + permissionService.deletePermission(recordFolder_1, testers, RMPermissionModel.FILING); + permissionService.deletePermission(recordFolder_2, testers, RMPermissionModel.FILING); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.ALLOWED); + + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.ALLOWED); + + // check frozen + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + Map params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "one"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "freeze", params); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.ALLOWED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "Two"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "freeze", params); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "unfreeze"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "unfreeze"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.ALLOWED); + + // Check closed + // should make no difference + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "closeRecordFolder"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "closeRecordFolder"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.ALLOWED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "openRecordFolder"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "openRecordFolder"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DESTROY_RECORDS, AccessStatus.ALLOWED); + + // cut off + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "cutoff", null); + recordsManagementActionService.executeRecordsManagementAction(record_2, "cutoff", null); + + // fix disposition + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + ndNodeRef = this.nodeService.getChildAssocs(recordFolder_1, RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + this.nodeService.setProperty(ndNodeRef, RecordsManagementModel.PROP_DISPOSITION_AS_OF, calendar.getTime()); + ndNodeRef = this.nodeService.getChildAssocs(record_2, RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + this.nodeService.setProperty(ndNodeRef, RecordsManagementModel.PROP_DISPOSITION_AS_OF, calendar.getTime()); + + // should delete even though transfer is next ..,. + + AuthenticationUtil.setFullyAuthenticatedUser(test_user); + nodeService.deleteNode(recordFolder_1); + nodeService.deleteNode(record_2); + + } + + public void testDestroyRecordsScheduledForDestructionCapability() + { + // Folder + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + + // folder level - not eligible all deny + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(rm_administrator, recordFolder_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(rm_records_manager, recordFolder_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(rm_security_officer, recordFolder_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(rm_administrator, record_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(rm_records_manager, record_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(rm_security_officer, record_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(rm_power_user, record_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(rm_user, record_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + + // record level - not eligible all deny + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(rm_administrator, recordFolder_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(rm_records_manager, recordFolder_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(rm_security_officer, recordFolder_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(rm_administrator, record_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(rm_records_manager, record_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(rm_security_officer, record_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(rm_power_user, record_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(rm_user, record_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + + // Set appropriate state - declare records and make eligible + + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.HOUR, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + + nodeService.setProperty(record_1, RecordsManagementModel.PROP_ORIGINATOR, "origValue"); + nodeService.setProperty(record_1, RecordsManagementModel.PROP_ORIGINATING_ORGANIZATION, "origOrgValue"); + nodeService.setProperty(record_1, RecordsManagementModel.PROP_PUBLICATION_DATE, new Date()); + nodeService.setProperty(record_1, ContentModel.PROP_TITLE, "titleValue"); + recordsManagementActionService.executeRecordsManagementAction(record_1, "declareRecord"); + + nodeService.setProperty(record_2, RecordsManagementModel.PROP_ORIGINATOR, "origValue"); + nodeService.setProperty(record_2, RecordsManagementModel.PROP_ORIGINATING_ORGANIZATION, "origOrgValue"); + nodeService.setProperty(record_2, RecordsManagementModel.PROP_PUBLICATION_DATE, new Date()); + nodeService.setProperty(record_2, ContentModel.PROP_TITLE, "titleValue"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "declareRecord"); + + NodeRef ndNodeRef = this.nodeService.getChildAssocs(recordFolder_1, RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + this.nodeService.setProperty(ndNodeRef, RecordsManagementModel.PROP_DISPOSITION_AS_OF, calendar.getTime()); + ndNodeRef = this.nodeService.getChildAssocs(record_2, RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + this.nodeService.setProperty(ndNodeRef, RecordsManagementModel.PROP_DISPOSITION_AS_OF, calendar.getTime()); + + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "cutoff", null); + recordsManagementActionService.executeRecordsManagementAction(record_2, "cutoff", null); + + ndNodeRef = this.nodeService.getChildAssocs(recordFolder_1, RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + this.nodeService.setProperty(ndNodeRef, RecordsManagementModel.PROP_DISPOSITION_AS_OF, calendar.getTime()); + ndNodeRef = this.nodeService.getChildAssocs(record_2, RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + this.nodeService.setProperty(ndNodeRef, RecordsManagementModel.PROP_DISPOSITION_AS_OF, calendar.getTime()); + + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "transfer", null); + recordsManagementActionService.executeRecordsManagementAction(record_2, "transfer", null); + // this completes both transfers :-) + recordsManagementActionService.executeRecordsManagementAction(getTransferObject(recordFolder_1), "transferComplete", null); + + ndNodeRef = this.nodeService.getChildAssocs(recordFolder_1, RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + this.nodeService.setProperty(ndNodeRef, RecordsManagementModel.PROP_DISPOSITION_AS_OF, calendar.getTime()); + ndNodeRef = this.nodeService.getChildAssocs(record_2, RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + this.nodeService.setProperty(ndNodeRef, RecordsManagementModel.PROP_DISPOSITION_AS_OF, calendar.getTime()); + + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "accession", null); + recordsManagementActionService.executeRecordsManagementAction(record_2, "accession", null); + + ndNodeRef = this.nodeService.getChildAssocs(recordFolder_1, RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + this.nodeService.setProperty(ndNodeRef, RecordsManagementModel.PROP_DISPOSITION_AS_OF, calendar.getTime()); + ndNodeRef = this.nodeService.getChildAssocs(record_2, RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + this.nodeService.setProperty(ndNodeRef, RecordsManagementModel.PROP_DISPOSITION_AS_OF, calendar.getTime()); + + // this completes both transfers :-) + recordsManagementActionService.executeRecordsManagementAction(getTransferObject(recordFolder_1), "transferComplete", null); + + ndNodeRef = this.nodeService.getChildAssocs(recordFolder_1, RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + this.nodeService.setProperty(ndNodeRef, RecordsManagementModel.PROP_DISPOSITION_AS_OF, calendar.getTime()); + ndNodeRef = this.nodeService.getChildAssocs(record_2, RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + this.nodeService.setProperty(ndNodeRef, RecordsManagementModel.PROP_DISPOSITION_AS_OF, calendar.getTime()); + + // folder level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.ALLOWED); + checkCapability(rm_administrator, recordFolder_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, recordFolder_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, recordFolder_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(rm_administrator, record_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(rm_records_manager, record_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(rm_security_officer, record_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(rm_power_user, record_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(rm_user, record_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + + // record level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(rm_administrator, recordFolder_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(rm_records_manager, recordFolder_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(rm_security_officer, recordFolder_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.ALLOWED); + checkCapability(rm_administrator, record_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, record_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, record_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(rm_power_user, record_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(rm_user, record_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + + // check person with no access and add read and write + // Filing + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS, true); + permissionService.setInheritParentPermissions(recordCategory_1, false); + permissionService.setInheritParentPermissions(recordCategory_2, false); + permissionService.setPermission(recordCategory_1, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordCategory_2, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.DECLARE_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.ALLOWED); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.DECLARE_RECORDS); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.ALLOWED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.DECLARE_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.ALLOWED); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.ALLOWED); + + permissionService.deletePermission(recordFolder_1, testers, RMPermissionModel.FILING); + permissionService.deletePermission(recordFolder_2, testers, RMPermissionModel.FILING); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.ALLOWED); + + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.ALLOWED); + + // check frozen + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + Map params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "one"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "freeze", params); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.ALLOWED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "Two"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "freeze", params); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "unfreeze"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "unfreeze"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.ALLOWED); + + // Check closed + // should make no difference + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "closeRecordFolder"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "closeRecordFolder"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.ALLOWED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "openRecordFolder"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "openRecordFolder"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION, AccessStatus.ALLOWED); + + // scheduled destroy + + AuthenticationUtil.setFullyAuthenticatedUser(test_user); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "destroy", null); + recordsManagementActionService.executeRecordsManagementAction(record_2, "destroy", null); + + } + + public void testDisplayRightsReportCapability() + { + // capability is checked above - just check permission assignments + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.DISPLAY_RIGHTS_REPORT, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.DISPLAY_RIGHTS_REPORT, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.DISPLAY_RIGHTS_REPORT, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.DISPLAY_RIGHTS_REPORT, AccessStatus.DENIED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.DISPLAY_RIGHTS_REPORT, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.DISPLAY_RIGHTS_REPORT, AccessStatus.DENIED); + } + + public void testEditDeclaredRecordMetadataCapability() + { + // Folder + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + + // folder level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_administrator, recordFolder_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_records_manager, recordFolder_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_security_officer, recordFolder_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_administrator, record_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_records_manager, record_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_security_officer, record_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_power_user, record_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_user, record_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + + // record level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_administrator, recordFolder_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_records_manager, recordFolder_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_security_officer, recordFolder_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_administrator, record_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_records_manager, record_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_security_officer, record_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_power_user, record_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_user, record_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + + // Set appropriate state - declare records and make eligible + + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.HOUR, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + + nodeService.setProperty(record_1, RecordsManagementModel.PROP_ORIGINATOR, "origValue"); + nodeService.setProperty(record_1, RecordsManagementModel.PROP_ORIGINATING_ORGANIZATION, "origOrgValue"); + nodeService.setProperty(record_1, RecordsManagementModel.PROP_PUBLICATION_DATE, new Date()); + nodeService.setProperty(record_1, ContentModel.PROP_TITLE, "titleValue"); + recordsManagementActionService.executeRecordsManagementAction(record_1, "declareRecord"); + + nodeService.setProperty(record_2, RecordsManagementModel.PROP_ORIGINATOR, "origValue"); + nodeService.setProperty(record_2, RecordsManagementModel.PROP_ORIGINATING_ORGANIZATION, "origOrgValue"); + nodeService.setProperty(record_2, RecordsManagementModel.PROP_PUBLICATION_DATE, new Date()); + nodeService.setProperty(record_2, ContentModel.PROP_TITLE, "titleValue"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "declareRecord"); + + // folder level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_administrator, recordFolder_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_records_manager, recordFolder_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_security_officer, recordFolder_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(rm_administrator, record_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, record_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, record_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_power_user, record_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_user, record_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + + // record level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_administrator, recordFolder_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_records_manager, recordFolder_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_security_officer, recordFolder_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(rm_administrator, record_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, record_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, record_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_power_user, record_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_user, record_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + + // check person with no access and add read and write + // Filing + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS, true); + permissionService.setInheritParentPermissions(recordCategory_1, false); + permissionService.setInheritParentPermissions(recordCategory_2, false); + permissionService.setPermission(recordCategory_1, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordCategory_2, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.ALLOWED); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.ALLOWED); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.ALLOWED); + + permissionService.deletePermission(recordFolder_1, testers, RMPermissionModel.FILING); + permissionService.deletePermission(recordFolder_2, testers, RMPermissionModel.FILING); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.ALLOWED); + + // check frozen + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + Map params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "one"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "freeze", params); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.ALLOWED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "Two"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "freeze", params); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "unfreeze"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "unfreeze"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.ALLOWED); + + // Check closed + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "closeRecordFolder"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "closeRecordFolder"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "openRecordFolder"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "openRecordFolder"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA, AccessStatus.ALLOWED); + + // try to modify + + publicNodeService.addAspect(record_1, ContentModel.ASPECT_OWNABLE, null); + Map properties = new HashMap(1); + properties.put(ContentModel.PROP_OWNER, "me"); + publicNodeService.addProperties(record_1, properties); + // move should fail ... + try + { + publicNodeService.moveNode(record_1, recordCategory_2, ContentModel.ASSOC_CONTAINS, TYPE_RECORD_FOLDER); + fail(); + } + catch (AccessDeniedException ade) + { + + } + publicNodeService.removeProperty(record_1, ContentModel.PROP_TITLE); + publicNodeService.setProperty(record_1, ContentModel.PROP_TITLE, "title"); + publicNodeService.addAspect(record_1, ContentModel.ASPECT_TEMPORARY, null); + publicNodeService.removeAspect(record_1, ContentModel.ASPECT_TEMPORARY); + publicNodeService.setProperties(record_1, publicNodeService.getProperties(record_1)); + try + { + // abstains + publicNodeService.setType(record_1, TYPE_RECORD_FOLDER); + fail(); + } + catch (AccessDeniedException ade) + { + + } + } + + public void testEditNonRecordMetadataCapability() + { + // Folder + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.ALLOWED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.ALLOWED); + checkPermission(rm_user, filePlan, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + + // folder level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_1, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(rm_administrator, recordFolder_1, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, recordFolder_1, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, recordFolder_1, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(rm_power_user, recordFolder_1, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(rm_user, recordFolder_1, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_1, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_administrator, record_1, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_records_manager, record_1, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_security_officer, record_1, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_power_user, record_1, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_user, record_1, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + + // record level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_2, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(rm_administrator, recordFolder_2, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, recordFolder_2, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, recordFolder_2, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(rm_power_user, recordFolder_2, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(rm_user, recordFolder_2, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_2, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_administrator, record_2, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_records_manager, record_2, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_security_officer, record_2, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_power_user, record_2, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_user, record_2, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + + // check person with no access and add read and write + // Filing + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS, true); + permissionService.setInheritParentPermissions(recordCategory_1, false); + permissionService.setInheritParentPermissions(recordCategory_2, false); + permissionService.setPermission(recordCategory_1, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordCategory_2, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.EDIT_NON_RECORD_METADATA, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.EDIT_NON_RECORD_METADATA); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.EDIT_NON_RECORD_METADATA, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + + permissionService.deletePermission(recordFolder_1, testers, RMPermissionModel.FILING); + permissionService.deletePermission(recordFolder_2, testers, RMPermissionModel.FILING); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + + // check frozen + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + Map params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "one"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "freeze", params); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "Two"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "freeze", params); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "unfreeze"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "unfreeze"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + + // Check closed + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "closeRecordFolder"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "closeRecordFolder"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "openRecordFolder"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "openRecordFolder"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.EDIT_NON_RECORD_METADATA, AccessStatus.DENIED); + + // try to modify + + publicNodeService.addAspect(recordFolder_1, ContentModel.ASPECT_OWNABLE, null); + Map properties = new HashMap(1); + properties.put(ContentModel.PROP_OWNER, "me"); + publicNodeService.addProperties(recordFolder_1, properties); + // move should fail ... + try + { + publicNodeService.moveNode(recordFolder_1, recordCategory_2, ContentModel.ASSOC_CONTAINS, TYPE_RECORD_FOLDER); + fail(); + } + catch (AccessDeniedException ade) + { + + } + publicNodeService.removeProperty(recordFolder_1, ContentModel.PROP_TITLE); + publicNodeService.setProperty(recordFolder_1, ContentModel.PROP_TITLE, "title"); + publicNodeService.addAspect(recordFolder_1, ContentModel.ASPECT_TEMPORARY, null); + publicNodeService.removeAspect(recordFolder_1, ContentModel.ASPECT_TEMPORARY); + publicNodeService.setProperties(recordFolder_1, publicNodeService.getProperties(recordFolder_1)); + try + { + // abstains + publicNodeService.setType(recordFolder_1, TYPE_RECORD_FOLDER); + fail(); + } + catch (AccessDeniedException ade) + { + + } + } + + public void testEditRecordMetadataCapability() + { + // Folder + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.ALLOWED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.ALLOWED); + checkPermission(rm_user, filePlan, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + + // folder level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_1, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_administrator, recordFolder_1, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_records_manager, recordFolder_1, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_security_officer, recordFolder_1, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_1, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_1, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_1, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(rm_administrator, record_1, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, record_1, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, record_1, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(rm_power_user, record_1, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(rm_user, record_1, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + + // record level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_2, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_administrator, recordFolder_2, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_records_manager, recordFolder_2, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_security_officer, recordFolder_2, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_2, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_2, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_2, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(rm_administrator, record_2, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, record_2, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, record_2, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(rm_power_user, record_2, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(rm_user, record_2, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + + // check person with no access and add read and write + // Filing + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS, true); + permissionService.setInheritParentPermissions(recordCategory_1, false); + permissionService.setInheritParentPermissions(recordCategory_2, false); + permissionService.setPermission(recordCategory_1, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordCategory_2, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.EDIT_RECORD_METADATA, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.ALLOWED); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.EDIT_RECORD_METADATA); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.EDIT_RECORD_METADATA, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.ALLOWED); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.ALLOWED); + + permissionService.deletePermission(recordFolder_1, testers, RMPermissionModel.FILING); + permissionService.deletePermission(recordFolder_2, testers, RMPermissionModel.FILING); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.ALLOWED); + + // check frozen + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + Map params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "one"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "freeze", params); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.ALLOWED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "Two"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "freeze", params); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "unfreeze"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "unfreeze"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.ALLOWED); + + // Check closed + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "closeRecordFolder"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "closeRecordFolder"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "openRecordFolder"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "openRecordFolder"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.EDIT_RECORD_METADATA, AccessStatus.ALLOWED); + + // try to modify + + publicNodeService.addAspect(record_1, ContentModel.ASPECT_OWNABLE, null); + Map properties = new HashMap(1); + properties.put(ContentModel.PROP_OWNER, "me"); + publicNodeService.addProperties(record_1, properties); + // move should fail ... + try + { + publicNodeService.moveNode(record_1, recordCategory_2, ContentModel.ASSOC_CONTAINS, TYPE_RECORD_FOLDER); + fail(); + } + catch (AccessDeniedException ade) + { + + } + publicNodeService.removeProperty(record_1, ContentModel.PROP_TITLE); + publicNodeService.setProperty(record_1, ContentModel.PROP_TITLE, "title"); + publicNodeService.addAspect(record_1, ContentModel.ASPECT_TEMPORARY, null); + publicNodeService.removeAspect(record_1, ContentModel.ASPECT_TEMPORARY); + publicNodeService.setProperties(record_1, publicNodeService.getProperties(record_1)); + try + { + // abstains + publicNodeService.setType(record_1, TYPE_RECORD_FOLDER); + fail(); + } + catch (AccessDeniedException ade) + { + + } + } + + public void testEditSelectionListsCapability() + { + // capability is checked above - just check permission assignments + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.EDIT_SELECTION_LISTS, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.EDIT_SELECTION_LISTS, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.EDIT_SELECTION_LISTS, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.EDIT_SELECTION_LISTS, AccessStatus.DENIED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.EDIT_SELECTION_LISTS, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.EDIT_SELECTION_LISTS, AccessStatus.DENIED); + } + + public void testEnableDisableAuditByTypesCapability() + { + // capability is checked above - just check permission assignments + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES, AccessStatus.DENIED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.ENABLE_DISABLE_AUDIT_BY_TYPES, AccessStatus.DENIED); + } + + public void testExportAuditCapability() + { + // capability is checked above - just check permission assignments + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.EXPORT_AUDIT, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.EXPORT_AUDIT, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.EXPORT_AUDIT, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.EXPORT_AUDIT, AccessStatus.DENIED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.EXPORT_AUDIT, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.EXPORT_AUDIT, AccessStatus.DENIED); + } + + public void testExtendRetentionPeriodOrFreezeCapability() + { + // freeze and unfreeze is part of most other tests - this jusr duplicates the basics ... + + // Folder + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + + // folder level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_1, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + checkCapability(rm_administrator, recordFolder_1, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, recordFolder_1, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, recordFolder_1, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_1, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_1, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_1, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + checkCapability(rm_administrator, record_1, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, record_1, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, record_1, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + checkCapability(rm_power_user, record_1, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + checkCapability(rm_user, record_1, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + + // record level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_2, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + checkCapability(rm_administrator, recordFolder_2, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, recordFolder_2, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, recordFolder_2, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_2, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_2, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_2, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + checkCapability(rm_administrator, record_2, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, record_2, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, record_2, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + checkCapability(rm_power_user, record_2, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + checkCapability(rm_user, record_2, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + + // check person with no access and add read and write + // Filing + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS, true); + permissionService.setInheritParentPermissions(recordCategory_1, false); + permissionService.setInheritParentPermissions(recordCategory_2, false); + permissionService.setPermission(recordCategory_1, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordCategory_2, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + + permissionService.deletePermission(recordFolder_1, testers, RMPermissionModel.FILING); + permissionService.deletePermission(recordFolder_2, testers, RMPermissionModel.FILING); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.DENIED); + + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + + // check frozen - can be in mutiple holds/freezes .. + + Map params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "one"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "freeze", params); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + + params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "Two"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "freeze", params); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "unfreeze"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "unfreeze"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + + // Check closed + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "closeRecordFolder"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "closeRecordFolder"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "openRecordFolder"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "openRecordFolder"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.EXTEND_RETENTION_PERIOD_OR_FREEZE, AccessStatus.ALLOWED); + + } + + public void testFileRecordsCapability() + { + // Folder + checkPermission(AuthenticationUtil.getSystemUserName(), recordFolder_1, RMPermissionModel.FILE_RECORDS, AccessStatus.ALLOWED); + checkPermission(rm_administrator, recordFolder_1, RMPermissionModel.FILE_RECORDS, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, recordFolder_1, RMPermissionModel.FILE_RECORDS, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, recordFolder_1, RMPermissionModel.FILE_RECORDS, AccessStatus.ALLOWED); + checkPermission(rm_power_user, recordFolder_1, RMPermissionModel.FILE_RECORDS, AccessStatus.ALLOWED); + checkPermission(rm_user, recordFolder_1, RMPermissionModel.FILE_RECORDS, AccessStatus.ALLOWED); + + // Record + checkPermission(AuthenticationUtil.getSystemUserName(), record_1, RMPermissionModel.FILE_RECORDS, AccessStatus.ALLOWED); + checkPermission(rm_administrator, record_1, RMPermissionModel.FILE_RECORDS, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, record_1, RMPermissionModel.FILE_RECORDS, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, record_1, RMPermissionModel.FILE_RECORDS, AccessStatus.ALLOWED); + checkPermission(rm_power_user, record_1, RMPermissionModel.FILE_RECORDS, AccessStatus.ALLOWED); + checkPermission(rm_user, record_1, RMPermissionModel.FILE_RECORDS, AccessStatus.ALLOWED); + + // folder level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_1, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_administrator, recordFolder_1, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_records_manager, recordFolder_1, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_security_officer, recordFolder_1, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_1, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_1, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_1, RMPermissionModel.FILE_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_administrator, record_1, RMPermissionModel.FILE_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, record_1, RMPermissionModel.FILE_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, record_1, RMPermissionModel.FILE_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_power_user, record_1, RMPermissionModel.FILE_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_user, record_1, RMPermissionModel.FILE_RECORDS, AccessStatus.ALLOWED); + + // record level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_2, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_administrator, recordFolder_2, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_records_manager, recordFolder_2, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_security_officer, recordFolder_2, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_2, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_2, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_2, RMPermissionModel.FILE_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_administrator, record_2, RMPermissionModel.FILE_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, record_2, RMPermissionModel.FILE_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, record_2, RMPermissionModel.FILE_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_power_user, record_2, RMPermissionModel.FILE_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_user, record_2, RMPermissionModel.FILE_RECORDS, AccessStatus.ALLOWED); + + // check person with no access and add read and write + // Filing + + checkCapability(test_user, recordFolder_1, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS, true); + permissionService.setInheritParentPermissions(recordCategory_1, false); + permissionService.setInheritParentPermissions(recordCategory_2, false); + permissionService.setPermission(recordCategory_1, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordCategory_2, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.FILE_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.FILE_RECORDS, AccessStatus.ALLOWED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.FILE_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.FILE_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.FILE_RECORDS, AccessStatus.ALLOWED); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.FILE_RECORDS); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.FILE_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.FILE_RECORDS, AccessStatus.ALLOWED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.FILE_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.FILE_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.FILE_RECORDS, AccessStatus.ALLOWED); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.FILE_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.FILE_RECORDS, AccessStatus.ALLOWED); + + permissionService.deletePermission(recordFolder_1, testers, RMPermissionModel.FILING); + permissionService.deletePermission(recordFolder_2, testers, RMPermissionModel.FILING); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.FILE_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.FILE_RECORDS, AccessStatus.ALLOWED); + + // check frozen + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + Map params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "one"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "freeze", params); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.FILE_RECORDS, AccessStatus.ALLOWED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "Two"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "freeze", params); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "unfreeze"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "unfreeze"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.FILE_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.FILE_RECORDS, AccessStatus.ALLOWED); + + // Check closed + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "closeRecordFolder"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "closeRecordFolder"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "openRecordFolder"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "openRecordFolder"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.FILE_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.FILE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.FILE_RECORDS, AccessStatus.ALLOWED); + + // Do some filing ... + + // create + + Map properties = new HashMap(1); + properties.put(ContentModel.PROP_NAME, "MyRecordCreate.txt"); + NodeRef newRecord_1 = this.publicNodeService.createNode(recordFolder_1, ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "MyRecord.txt"), ContentModel.TYPE_CONTENT, properties).getChildRef(); + + // Set the content (relies on owner in the DM side until it becode RM ified ...) + ContentWriter writer = this.publicContentService.getWriter(newRecord_1, ContentModel.PROP_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.setEncoding("UTF-8"); + writer.putContent("There is some content in this record"); + + assertFalse(recordsManagementService.isFilePlanComponent(newRecord_1)); + recordsManagementActionService.executeRecordsManagementAction(newRecord_1, "file"); + assertTrue(recordsManagementService.isFilePlanComponent(newRecord_1)); + + properties = new HashMap(1); + properties.put(ContentModel.PROP_NAME, "MyRecordCreate.txt"); + NodeRef newRecord_2 = this.publicNodeService.createNode(recordFolder_2, ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "MyRecord.txt"), ContentModel.TYPE_CONTENT, properties).getChildRef(); + + // Set the content + writer = this.publicContentService.getWriter(newRecord_2, ContentModel.PROP_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.setEncoding("UTF-8"); + writer.putContent("There is some content in this record"); + + recordsManagementActionService.executeRecordsManagementAction(newRecord_2, "file"); + + // update with permissions in place ... + + writer = this.publicContentService.getWriter(newRecord_1, ContentModel.PROP_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.setEncoding("UTF-8"); + writer.putContent("There is some updated content in this record"); + + writer = this.publicContentService.getWriter(newRecord_2, ContentModel.PROP_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.setEncoding("UTF-8"); + writer.putContent("There is some content in this record"); + } + + public void testMakeOptionalPropertiesMandatoryCapability() + { + // capability is checked above - just check permission assignments + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY, AccessStatus.DENIED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.MAKE_OPTIONAL_PARAMETERS_MANDATORY, AccessStatus.DENIED); + } + + public void testManageAccessControlsCapability() + { + // capability is checked above - just check permission assignments + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.MANAGE_ACCESS_CONTROLS, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.MANAGE_ACCESS_CONTROLS, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.MANAGE_ACCESS_CONTROLS, AccessStatus.DENIED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.MANAGE_ACCESS_CONTROLS, AccessStatus.DENIED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.MANAGE_ACCESS_CONTROLS, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.MANAGE_ACCESS_CONTROLS, AccessStatus.DENIED); + } + + public void testManageAccessRightsCapability() + { + // capability is checked above - just check permission assignments + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.MANAGE_ACCESS_RIGHTS, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.MANAGE_ACCESS_RIGHTS, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.MANAGE_ACCESS_RIGHTS, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.MANAGE_ACCESS_RIGHTS, AccessStatus.DENIED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.MANAGE_ACCESS_RIGHTS, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.MANAGE_ACCESS_RIGHTS, AccessStatus.DENIED); + } + + public void testManuallyChangeDispositionDatesCapability() + { + // TODO: The action is not yet done + } + + public void testMapClassificationGuideMetadataCapability() + { + // capability is checked above - just check permission assignments + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA, AccessStatus.DENIED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.MAP_CLASSIFICATION_GUIDE_METADATA, AccessStatus.DENIED); + } + + public void testMapEmailMetadataCapability() + { + // capability is checked above - just check permission assignments + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.MAP_EMAIL_METADATA, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.MAP_EMAIL_METADATA, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.MAP_EMAIL_METADATA, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.MAP_EMAIL_METADATA, AccessStatus.DENIED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.MAP_EMAIL_METADATA, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.MAP_EMAIL_METADATA, AccessStatus.DENIED); + } + + public void testMoveRecordsCapability() + { + // capability is checked above - just check permission assignments + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.MOVE_RECORDS, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.MOVE_RECORDS, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.MOVE_RECORDS, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.MOVE_RECORDS, AccessStatus.DENIED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.MOVE_RECORDS, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.MOVE_RECORDS, AccessStatus.DENIED); + } + + public void testPasswordControlCapability() + { + // capability is checked above - just check permission assignments + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.PASSWORD_CONTROL, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.PASSWORD_CONTROL, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.PASSWORD_CONTROL, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.PASSWORD_CONTROL, AccessStatus.DENIED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.PASSWORD_CONTROL, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.PASSWORD_CONTROL, AccessStatus.DENIED); + } + + public void testPlanningReviewCyclesCapability() + { + // TODO: Waiting for the appropriate action + } + + public void testReOpenFoldersCapability() + { + // Folder + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + checkPermission(rm_user, filePlan, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + + // folder level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_administrator, recordFolder_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, recordFolder_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, recordFolder_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_power_user, recordFolder_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_user, recordFolder_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_administrator, record_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_records_manager, record_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_security_officer, record_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_power_user, record_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_user, record_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + + // record level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_administrator, recordFolder_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, recordFolder_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, recordFolder_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_power_user, recordFolder_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_user, recordFolder_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_administrator, record_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_records_manager, record_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_security_officer, record_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_power_user, record_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_user, record_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + + // Check closed + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "closeRecordFolder"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "closeRecordFolder"); + + // folder level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_administrator, recordFolder_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, recordFolder_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, recordFolder_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_power_user, recordFolder_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_user, recordFolder_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_administrator, record_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_records_manager, record_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_security_officer, record_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_power_user, record_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_user, record_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + + // record level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_administrator, recordFolder_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, recordFolder_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, recordFolder_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_power_user, recordFolder_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + checkCapability(rm_user, recordFolder_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_administrator, record_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_records_manager, record_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_security_officer, record_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_power_user, record_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(rm_user, record_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + + // check person with no access and add read and write + // Filing + + checkCapability(test_user, recordFolder_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS, true); + permissionService.setInheritParentPermissions(recordCategory_1, false); + permissionService.setInheritParentPermissions(recordCategory_2, false); + permissionService.setPermission(recordCategory_1, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordCategory_2, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.RE_OPEN_FOLDERS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.RE_OPEN_FOLDERS); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.RE_OPEN_FOLDERS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + + permissionService.deletePermission(recordFolder_1, testers, RMPermissionModel.FILING); + permissionService.deletePermission(recordFolder_2, testers, RMPermissionModel.FILING); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + + // check frozen + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + Map params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "one"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "freeze", params); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "Two"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "freeze", params); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "unfreeze"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "unfreeze"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "openRecordFolder"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "openRecordFolder"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.ALLOWED); + checkCapability(test_user, record_2, RMPermissionModel.RE_OPEN_FOLDERS, AccessStatus.DENIED); + + } + + public void testSelectAuditMetadataCapability() + { + // capability is checked above - just check permission assignments + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.SELECT_AUDIT_METADATA, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.SELECT_AUDIT_METADATA, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.SELECT_AUDIT_METADATA, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.SELECT_AUDIT_METADATA, AccessStatus.DENIED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.SELECT_AUDIT_METADATA, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.SELECT_AUDIT_METADATA, AccessStatus.DENIED); + } + + public void testTriggerAnEventCapability() + { + // TODO: Waiting for action + } + + public void testUndeclareRecordsCapability() + { + // Folder + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + + // folder level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_administrator, recordFolder_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_records_manager, recordFolder_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_security_officer, recordFolder_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_administrator, record_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_records_manager, record_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_security_officer, record_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_power_user, record_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_user, record_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + + // record level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_administrator, recordFolder_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_records_manager, recordFolder_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_security_officer, recordFolder_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_administrator, record_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_records_manager, record_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_security_officer, record_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_power_user, record_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_user, record_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + + // Set appropriate state - declare records and make eligible + + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.HOUR, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + + nodeService.setProperty(record_1, RecordsManagementModel.PROP_ORIGINATOR, "origValue"); + nodeService.setProperty(record_1, RecordsManagementModel.PROP_ORIGINATING_ORGANIZATION, "origOrgValue"); + nodeService.setProperty(record_1, RecordsManagementModel.PROP_PUBLICATION_DATE, new Date()); + nodeService.setProperty(record_1, ContentModel.PROP_TITLE, "titleValue"); + recordsManagementActionService.executeRecordsManagementAction(record_1, "declareRecord"); + + nodeService.setProperty(record_2, RecordsManagementModel.PROP_ORIGINATOR, "origValue"); + nodeService.setProperty(record_2, RecordsManagementModel.PROP_ORIGINATING_ORGANIZATION, "origOrgValue"); + nodeService.setProperty(record_2, RecordsManagementModel.PROP_PUBLICATION_DATE, new Date()); + nodeService.setProperty(record_2, ContentModel.PROP_TITLE, "titleValue"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "declareRecord"); + + // folder level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_administrator, recordFolder_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_records_manager, recordFolder_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_security_officer, recordFolder_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_administrator, record_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, record_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, record_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_power_user, record_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_user, record_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + + // record level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_administrator, recordFolder_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_records_manager, recordFolder_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_security_officer, recordFolder_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_administrator, record_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, record_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, record_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_power_user, record_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(rm_user, record_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + + // check person with no access and add read and write + // Filing + + checkCapability(test_user, recordFolder_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS, true); + permissionService.setInheritParentPermissions(recordCategory_1, false); + permissionService.setInheritParentPermissions(recordCategory_2, false); + permissionService.setPermission(recordCategory_1, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordCategory_2, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.UNDECLARE_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.ALLOWED); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.UNDECLARE_RECORDS); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.UNDECLARE_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.ALLOWED); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.ALLOWED); + + permissionService.deletePermission(recordFolder_1, testers, RMPermissionModel.FILING); + permissionService.deletePermission(recordFolder_2, testers, RMPermissionModel.FILING); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.ALLOWED); + + // check frozen + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + Map params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "one"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "freeze", params); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.ALLOWED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "Two"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "freeze", params); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "unfreeze"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "unfreeze"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.ALLOWED); + + // Check closed + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "closeRecordFolder"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "closeRecordFolder"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.ALLOWED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "openRecordFolder"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "openRecordFolder"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.ALLOWED); + + // try undeclare + + AuthenticationUtil.setFullyAuthenticatedUser(test_user); + try + { + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "undeclareRecord", null); + fail(); + } + catch (AccessDeniedException ade) + { + + } + recordsManagementActionService.executeRecordsManagementAction(record_1, "undeclareRecord"); + try + { + recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "undeclareRecord", null); + fail(); + } + catch (AccessDeniedException ade) + { + + } + recordsManagementActionService.executeRecordsManagementAction(record_2, "undeclareRecord"); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.UNDECLARE_RECORDS, AccessStatus.DENIED); + } + + public void testUnfreezeCapability() + { + // freeze and unfreeze is part of most other tests - this jusr duplicates the basics ... + + // Folder + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.UNFREEZE, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.UNFREEZE, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.UNFREEZE, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + + // folder level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_1, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(rm_administrator, recordFolder_1, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(rm_records_manager, recordFolder_1, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(rm_security_officer, recordFolder_1, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_1, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_1, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_1, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(rm_administrator, record_1, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(rm_records_manager, record_1, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(rm_security_officer, record_1, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(rm_power_user, record_1, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(rm_user, record_1, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + + // record level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_2, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(rm_administrator, recordFolder_2, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(rm_records_manager, recordFolder_2, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(rm_security_officer, recordFolder_2, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_2, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_2, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_2, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(rm_administrator, record_2, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(rm_records_manager, record_2, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(rm_security_officer, record_2, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(rm_power_user, record_2, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(rm_user, record_2, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + Map params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "one"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "freeze", params); + params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "Two"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "freeze", params); + + // folder level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_1, RMPermissionModel.UNFREEZE, AccessStatus.ALLOWED); + checkCapability(rm_administrator, recordFolder_1, RMPermissionModel.UNFREEZE, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, recordFolder_1, RMPermissionModel.UNFREEZE, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, recordFolder_1, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_1, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_1, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_1, RMPermissionModel.UNFREEZE, AccessStatus.ALLOWED); + checkCapability(rm_administrator, record_1, RMPermissionModel.UNFREEZE, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, record_1, RMPermissionModel.UNFREEZE, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, record_1, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(rm_power_user, record_1, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(rm_user, record_1, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + + // record level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_2, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(rm_administrator, recordFolder_2, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(rm_records_manager, recordFolder_2, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(rm_security_officer, recordFolder_2, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_2, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_2, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_2, RMPermissionModel.UNFREEZE, AccessStatus.ALLOWED); + checkCapability(rm_administrator, record_2, RMPermissionModel.UNFREEZE, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, record_2, RMPermissionModel.UNFREEZE, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, record_2, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(rm_power_user, record_2, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(rm_user, record_2, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + + // check person with no access and add read and write + // Filing + + checkCapability(test_user, recordFolder_1, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS, true); + permissionService.setInheritParentPermissions(recordCategory_1, false); + permissionService.setInheritParentPermissions(recordCategory_2, false); + permissionService.setPermission(recordCategory_1, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordCategory_2, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.UNFREEZE, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.UNFREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.UNFREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.UNFREEZE, AccessStatus.ALLOWED); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.UNFREEZE); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.UNFREEZE, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.UNFREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.UNFREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.UNFREEZE, AccessStatus.ALLOWED); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.UNFREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.UNFREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.UNFREEZE, AccessStatus.ALLOWED); + + permissionService.deletePermission(recordFolder_1, testers, RMPermissionModel.FILING); + permissionService.deletePermission(recordFolder_2, testers, RMPermissionModel.FILING); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(test_user, record_1, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, recordFolder_1, RMPermissionModel.UNFREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.UNFREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.UNFREEZE, AccessStatus.ALLOWED); + + // check frozen - can be in mutiple holds/freezes .. + + checkCapability(test_user, recordFolder_1, RMPermissionModel.UNFREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, record_1, RMPermissionModel.UNFREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, recordFolder_2, RMPermissionModel.UNFREEZE, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.UNFREEZE, AccessStatus.ALLOWED); + + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "unfreeze"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "unfreeze"); + + } + + public void testUpdateClassificationDatesCapability() + { + // capability is checked above - just check permission assignments + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.UPDATE_CLASSIFICATION_DATES, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.UPDATE_CLASSIFICATION_DATES, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.UPDATE_CLASSIFICATION_DATES, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.UPDATE_CLASSIFICATION_DATES, AccessStatus.ALLOWED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.UPDATE_CLASSIFICATION_DATES, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.UPDATE_CLASSIFICATION_DATES, AccessStatus.DENIED); + } + + public void testUpdateExemptionCategoriesCapability() + { + // capability is checked above - just check permission assignments + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES, AccessStatus.ALLOWED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.UPDATE_EXEMPTION_CATEGORIES, AccessStatus.DENIED); + } + + public void testUpdateTriggerDatesCapability() + { + // TODO: waiting for action + } + + public void testUpdateVitalRecordCycleInformationCapability() + { + // TODO: ? + } + + public void testUpgradeDowngradeAndDeclassifyRecordsCapability() + { + // capability is checked above - just check permission assignments + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, AccessStatus.ALLOWED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.UPGRADE_DOWNGRADE_AND_DECLASSIFY_RECORDS, AccessStatus.DENIED); + } + + public void testViewRecordsCapability() + { + // capability is checked above - just check permission assignments + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.VIEW_RECORDS, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.VIEW_RECORDS, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.VIEW_RECORDS, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.VIEW_RECORDS, AccessStatus.ALLOWED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.VIEW_RECORDS, AccessStatus.ALLOWED); + checkPermission(rm_user, filePlan, RMPermissionModel.VIEW_RECORDS, AccessStatus.ALLOWED); + // already tested in many places above + } + + public void testViewUpdateReasonsForFreezeCapability() + { + // Folder + checkPermission(AuthenticationUtil.getSystemUserName(), filePlan, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.ALLOWED); + checkPermission(rm_administrator, filePlan, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.ALLOWED); + checkPermission(rm_records_manager, filePlan, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.ALLOWED); + checkPermission(rm_security_officer, filePlan, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkPermission(rm_power_user, filePlan, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkPermission(rm_user, filePlan, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + + // folder level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_1, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(rm_administrator, recordFolder_1, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(rm_records_manager, recordFolder_1, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(rm_security_officer, recordFolder_1, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_1, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_1, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_1, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(rm_administrator, record_1, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(rm_records_manager, record_1, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(rm_security_officer, record_1, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(rm_power_user, record_1, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(rm_user, record_1, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + + // record level + + checkCapability(AuthenticationUtil.getSystemUserName(), recordFolder_2, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(rm_administrator, recordFolder_2, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(rm_records_manager, recordFolder_2, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(rm_security_officer, recordFolder_2, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(rm_power_user, recordFolder_2, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(rm_user, recordFolder_2, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), record_2, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(rm_administrator, record_2, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(rm_records_manager, record_2, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(rm_security_officer, record_2, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(rm_power_user, record_2, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(rm_user, record_2, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + Map params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "one"); + recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "freeze", params); + params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "Two"); + recordsManagementActionService.executeRecordsManagementAction(record_2, "freeze", params); + + // folder level + + checkCapability(AuthenticationUtil.getSystemUserName(), getHold(recordFolder_1), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.ALLOWED); + checkCapability(rm_administrator, getHold(recordFolder_1), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, getHold(recordFolder_1), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, getHold(recordFolder_1), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(rm_power_user, getHold(recordFolder_1), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(rm_user, getHold(recordFolder_1), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), getHold(record_1), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.ALLOWED); + checkCapability(rm_administrator, getHold(record_1), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, getHold(record_1), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, getHold(record_1), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(rm_power_user, getHold(record_1), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(rm_user, getHold(record_1), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + + // record level + + checkCapability(AuthenticationUtil.getSystemUserName(), getHold(recordFolder_2), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(rm_administrator, getHold(recordFolder_2), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(rm_records_manager, getHold(recordFolder_2), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(rm_security_officer, getHold(recordFolder_2), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(rm_power_user, getHold(recordFolder_2), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(rm_user, getHold(recordFolder_2), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + + checkCapability(AuthenticationUtil.getSystemUserName(), getHold(record_2), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.ALLOWED); + checkCapability(rm_administrator, getHold(record_2), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.ALLOWED); + checkCapability(rm_records_manager, getHold(record_2), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.ALLOWED); + checkCapability(rm_security_officer, getHold(record_2), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(rm_power_user, getHold(record_2), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(rm_user, getHold(record_2), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + + // check person with no access and add read and write + // Filing + + checkCapability(test_user, getHold(recordFolder_1), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(test_user, getHold(record_1), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(test_user, getHold(recordFolder_2), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(test_user, record_2, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS, true); + permissionService.setPermission(filePlan, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setInheritParentPermissions(recordCategory_1, false); + permissionService.setInheritParentPermissions(recordCategory_2, false); + permissionService.setPermission(recordCategory_1, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordCategory_2, testers, RMPermissionModel.READ_RECORDS, true); + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, getHold(recordFolder_1), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(test_user, getHold(record_1), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(test_user, getHold(recordFolder_2), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(test_user, getHold(record_2), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, true); + + checkCapability(test_user, getHold(recordFolder_1), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, getHold(record_1), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, getHold(recordFolder_2), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(test_user, getHold(record_2), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.ALLOWED); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE); + + checkCapability(test_user, getHold(recordFolder_1), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(test_user, getHold(record_1), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(test_user, getHold(recordFolder_2), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(test_user, getHold(record_2), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, true); + + checkCapability(test_user, getHold(recordFolder_1), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, getHold(record_1), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, getHold(recordFolder_2), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(test_user, getHold(record_2), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.ALLOWED); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS); + + checkCapability(test_user, getHold(recordFolder_1), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(test_user, getHold(record_1), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(test_user, getHold(recordFolder_2), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(test_user, getHold(record_2), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_RECORDS, true); + + checkCapability(test_user, getHold(recordFolder_1), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, getHold(record_1), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, getHold(recordFolder_2), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(test_user, getHold(record_2), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.ALLOWED); + + permissionService.deletePermission(recordFolder_1, testers, RMPermissionModel.FILING); + permissionService.deletePermission(recordFolder_2, testers, RMPermissionModel.FILING); + + checkCapability(test_user, getHold(recordFolder_1), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, getHold(record_1), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, getHold(recordFolder_2), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(test_user, getHold(record_2), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.ALLOWED); + + permissionService.setPermission(recordFolder_1, testers, RMPermissionModel.FILING, true); + permissionService.setPermission(recordFolder_2, testers, RMPermissionModel.FILING, true); + + checkCapability(test_user, getHold(recordFolder_1), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, getHold(record_1), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, getHold(recordFolder_2), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(test_user, getHold(record_2), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.ALLOWED); + + // check frozen - can be in multiple holds/freezes .. + + checkCapability(test_user, getHold(recordFolder_1), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, getHold(record_1), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.ALLOWED); + checkCapability(test_user, getHold(recordFolder_2), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.DENIED); + checkCapability(test_user, getHold(record_2), RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, AccessStatus.ALLOWED); + + // TODO: property is not yet duplicated, waiting for action. + + // test filter - from the freeze object + + Map returned = publicNodeService.getProperties(getHold(recordFolder_1)); + assertTrue(returned.containsKey(RecordsManagementModel.PROP_HOLD_REASON)); + assertNotNull(publicNodeService.getProperty(getHold(recordFolder_1), RecordsManagementModel.PROP_HOLD_REASON)); + + permissionService.deletePermission(filePlan, testers, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE); + + returned = publicNodeService.getProperties(getHold(recordFolder_1)); + assertFalse(returned.containsKey(RecordsManagementModel.PROP_HOLD_REASON)); + try + { + publicNodeService.getProperty(getHold(recordFolder_1), RecordsManagementModel.PROP_HOLD_REASON); + fail(); + } + catch (AccessDeniedException ade) + { + + } + + // test query + + // update + + permissionService.setPermission(filePlan, testers, RMPermissionModel.FILING, true); + try + { + publicNodeService.setProperty(getHold(recordFolder_1), RecordsManagementModel.PROP_HOLD_REASON, "meep"); + fail(); + } + catch (AccessDeniedException ade) + { + + } + permissionService.setPermission(filePlan, testers, RMPermissionModel.VIEW_UPDATE_REASONS_FOR_FREEZE, true); + // TODO: fix reject by updateProperties - no capabilty lets it through even though not protected + // publicNodeService.setProperty(getHold(recordFolder_1), RecordsManagementModel.PROP_HOLD_REASON, "meep"); + + // update by action + + // + } + + private NodeRef getHold(NodeRef held) + { + List holdAssocs = nodeService.getChildAssocs(filePlan, RecordsManagementModel.ASSOC_HOLDS, RegexQNamePattern.MATCH_ALL); + for (ChildAssociationRef holdAssoc : holdAssocs) + { + List freezeAssocs = nodeService.getChildAssocs(holdAssoc.getChildRef()); + for (ChildAssociationRef inHold : freezeAssocs) + { + if (inHold.getChildRef().equals(held)) + { + return holdAssoc.getChildRef(); + } + List heldFolderChildren = nodeService.getChildAssocs(inHold.getChildRef()); + for (ChildAssociationRef car : heldFolderChildren) + { + if (car.getChildRef().equals(held)) + { + return holdAssoc.getChildRef(); + } + } + } + } + return held; + } + + private void check(Map access, String name, AccessStatus accessStatus) + { + Capability capability = recordsManagementSecurityService.getCapability(name); + assertNotNull(capability); + assertEquals(accessStatus, access.get(capability)); + } + + private static ImporterBinding REPLACE_BINDING = new ImporterBinding() + { + + public UUID_BINDING getUUIDBinding() + { + return UUID_BINDING.UPDATE_EXISTING; + } + + public String getValue(String key) + { + return null; + } + + public boolean allowReferenceWithinTransaction() + { + return false; + } + + public QName[] getExcludedClasses() + { + return null; + } + + }; + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/system/DODDataLoadSystemTest.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/system/DODDataLoadSystemTest.java new file mode 100644 index 0000000000..ce1c02fc94 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/system/DODDataLoadSystemTest.java @@ -0,0 +1,78 @@ +/* + * 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.module.org_alfresco_module_rm.test.system; + +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService; +import org.alfresco.module.org_alfresco_module_rm.test.util.TestUtilities; +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.cmr.view.ImporterService; +import org.alfresco.util.BaseSpringTest; + +/** + * + * + * @author Roy Wetherall + */ +public class DODDataLoadSystemTest extends BaseSpringTest +{ + private NodeService nodeService; + private AuthenticationComponent authenticationComponent; + private ImporterService importer; + private PermissionService permissionService; + private SearchService searchService; + private RecordsManagementService rmService; + private RecordsManagementActionService rmActionService; + + @Override + protected void onSetUpInTransaction() throws Exception + { + super.onSetUpInTransaction(); + + // Get the service required in the tests + this.nodeService = (NodeService)this.applicationContext.getBean("NodeService"); + this.authenticationComponent = (AuthenticationComponent)this.applicationContext.getBean("authenticationComponent"); + this.importer = (ImporterService)this.applicationContext.getBean("ImporterService"); + this.permissionService = (PermissionService)this.applicationContext.getBean("PermissionService"); + searchService = (SearchService)applicationContext.getBean("SearchService"); + rmService = (RecordsManagementService)applicationContext.getBean("RecordsManagementService"); + rmActionService = (RecordsManagementActionService)applicationContext.getBean("RecordsManagementActionService"); + + + // Set the current security context as admin + this.authenticationComponent.setCurrentUser(AuthenticationUtil.getSystemUserName()); + } + + public void testSetup() + { + // NOOP + } + + public void testLoadFilePlanData() + { + TestUtilities.loadFilePlanData(applicationContext); + + setComplete(); + endTransaction(); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/system/NotificationServiceHelperSystemTest.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/system/NotificationServiceHelperSystemTest.java new file mode 100644 index 0000000000..6577dc21e1 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/system/NotificationServiceHelperSystemTest.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2009-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.module.org_alfresco_module_rm.test.system; + +import java.util.ArrayList; +import java.util.List; + +import org.alfresco.module.org_alfresco_module_rm.notification.RecordsManagementNotificationHelper; +import org.alfresco.module.org_alfresco_module_rm.security.Role; +import org.alfresco.module.org_alfresco_module_rm.test.util.BaseRMTestCase; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.AuthorityService; +import org.alfresco.service.cmr.security.MutableAuthenticationService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.util.GUID; +import org.alfresco.util.PropertyMap; + + +/** + * Notification helper (system) test + * + * @author Roy Wetherall + */ +public class NotificationServiceHelperSystemTest extends BaseRMTestCase +{ + private static final String NOTIFICATION_ROLE = "RecordsManager"; + private static final String EMAIL_ADDRESS = "roy.wetherall@alfreso.com"; + + /** Services */ + private RecordsManagementNotificationHelper notificationHelper; + private MutableAuthenticationService authenticationService; + private PersonService personService; + private AuthorityService authorityService; + + /** Test data */ + private NodeRef record; + private List records; + private String userName; + private NodeRef person; + + @Override + protected void initServices() + { + super.initServices(); + + // Get the notification helper + notificationHelper = (RecordsManagementNotificationHelper)applicationContext.getBean("recordsManagementNotificationHelper"); + authenticationService = (MutableAuthenticationService)applicationContext.getBean("AuthenticationService"); + authorityService = (AuthorityService)applicationContext.getBean("AuthorityService"); + personService = (PersonService)applicationContext.getBean("PersonService"); + } + + @Override + protected void setupTestData() + { + super.setupTestData(); + + retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + + // Create a user + userName = GUID.generate(); + authenticationService.createAuthentication(userName, "".toCharArray()); + PropertyMap props = new PropertyMap(); + props.put(PROP_USERNAME, userName); + props.put(PROP_FIRSTNAME, "Test"); + props.put(PROP_LASTNAME, "User"); + props.put(PROP_EMAIL, EMAIL_ADDRESS); + person = personService.createPerson(props); + + // Find the authority for the given role + Role role = securityService.getRole(filePlan, NOTIFICATION_ROLE); + assertNotNull("Notification role could not be retrieved", role); + String roleGroup = role.getRoleGroupName(); + assertNotNull("Notification role group can not be null.", roleGroup); + + // Add user to notification role group + authorityService.addAuthority(roleGroup, userName); + + return null; + } + }); + } + + @Override + protected void setupTestDataImpl() + { + super.setupTestDataImpl(); + + // Create a few test records + record = createRecord(rmFolder, "recordOne"); + NodeRef record2 = createRecord(rmFolder, "recordTwo"); + NodeRef record3 = createRecord(rmFolder, "recordThree"); + + records = new ArrayList(3); + records.add(record); + records.add(record2); + records.add(record3); + } + + @Override + protected void tearDownImpl() + { + super.tearDownImpl(); + + // Delete the person and user + personService.deletePerson(person); + } + + public void testSendDueForReviewNotification() + { + doTestInTransaction(new Test() + { + @Override + public Void run() + { + notificationHelper.recordsDueForReviewEmailNotification(records); + return null; + } + }); + } + + public void testSendSupersededNotification() + { + doTestInTransaction(new Test() + { + @Override + public Void run() + { + notificationHelper.recordSupersededEmailNotification(record); + return null; + } + }); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/system/PerformanceDataLoadSystemTest.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/system/PerformanceDataLoadSystemTest.java new file mode 100644 index 0000000000..48de8c4a32 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/system/PerformanceDataLoadSystemTest.java @@ -0,0 +1,253 @@ +/* + * 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.module.org_alfresco_module_rm.test.system; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.transaction.UserTransaction; + +import junit.framework.TestCase; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService; +import org.alfresco.module.org_alfresco_module_rm.dod5015.DOD5015Model; +import org.alfresco.module.org_alfresco_module_rm.identifier.IdentifierService; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.security.authentication.AuthenticationComponent; +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; +import org.alfresco.service.cmr.repository.Period; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ApplicationContextHelper; +import org.springframework.context.ApplicationContext; + +/** + * @author Roy Wetherall + */ +public class PerformanceDataLoadSystemTest extends TestCase implements RecordsManagementModel, DOD5015Model +{ + private ApplicationContext appContext; + private AuthenticationComponent authenticationComponent; + private RecordsManagementService rmService; + private DispositionService dispositionService; + private TransactionService transactionService; + private NodeService nodeService; + private ContentService contentService; + private IdentifierService identifierService; + + UserTransaction userTransaction; + + private int SERIES_COUNT = 1; + private int CATEGORY_COUNT = 1; + private int RECORD_FOLDER_COUNT = 1; + private int RECORD_COUNT = 700; + + @Override + protected void setUp() throws Exception + { + appContext = ApplicationContextHelper.getApplicationContext(); + authenticationComponent = (AuthenticationComponent)appContext.getBean("authenticationComponent"); + transactionService = (TransactionService)appContext.getBean("transactionService"); + nodeService = (NodeService)appContext.getBean("nodeService"); + rmService = (RecordsManagementService)appContext.getBean("recordsManagementService"); + contentService = (ContentService)appContext.getBean("contentService"); + identifierService = (IdentifierService)appContext.getBean("identifierService"); + dispositionService = (DispositionService)appContext.getBean("dispositionService"); + + // Set authentication + authenticationComponent.setCurrentUser("admin"); + + // Start transaction + userTransaction = transactionService.getUserTransaction(); + userTransaction.begin(); + } + + @Override + protected void tearDown() throws Exception + { + userTransaction.commit(); + } + + public void testLoadTestData() throws Exception + { + // Get the file plan node + List roots = rmService.getFilePlans(); + if (roots.size() != 1) + { + fail("There is more than one root to load the test data into."); + } + NodeRef filePlan = roots.get(0); + + for (int i = 0; i < SERIES_COUNT; i++) + { + // Create the series + createSeries(filePlan, i); + } + } + + private void createSeries(NodeRef filePlan, int index) throws Exception + { + String name = genName("series-", index, "-" + System.currentTimeMillis()); + Map properties = new HashMap(2); + properties.put(ContentModel.PROP_NAME, name); + properties.put(PROP_IDENTIFIER, identifierService.generateIdentifier(TYPE_RECORD_CATEGORY, filePlan)); + NodeRef series = nodeService.createNode( + filePlan, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, name), + TYPE_RECORD_CATEGORY, + properties).getChildRef(); + + System.out.println("Created series '" + name); + + // Create the categories + for (int i = 0; i < CATEGORY_COUNT; i++) + { + createCategory(series, i); + } + } + + private void createCategory(NodeRef series, int index) throws Exception + { + String name = genName("category-", index); + Map properties = new HashMap(7); + properties.put(ContentModel.PROP_NAME, name); + properties.put(ContentModel.PROP_DESCRIPTION, "Test category"); + NodeRef cat = nodeService.createNode( + series, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, name), + TYPE_RECORD_CATEGORY, + properties).getChildRef(); + + // Need to close the transaction and reopen to kick off required initialisation behaviour + userTransaction.commit(); + userTransaction = transactionService.getUserTransaction(); + userTransaction.begin(); + + properties = nodeService.getProperties(cat); + //properties.put(PROP_IDENTIFIER, identifierService.generateIdentifier(series)); + properties.put(PROP_VITAL_RECORD_INDICATOR, true); + properties.put(PROP_REVIEW_PERIOD, new Period("week|1")); + nodeService.setProperties(cat, properties); + + // Get the disposition schedule + DispositionSchedule ds = dispositionService.getDispositionSchedule(cat); + properties = nodeService.getProperties(ds.getNodeRef()); + properties.put(PROP_DISPOSITION_AUTHORITY, "Disposition Authority"); + properties.put(PROP_DISPOSITION_INSTRUCTIONS, "Test disposition"); + nodeService.setProperties(ds.getNodeRef(), properties); + + // Add cutoff disposition action + Map actionParams = new HashMap(2); + actionParams.put(PROP_DISPOSITION_ACTION_NAME, "cutoff"); + actionParams.put(PROP_DISPOSITION_PERIOD, new Period("day|1")); + dispositionService.addDispositionActionDefinition(ds, actionParams); + + // Add delete disposition action + actionParams = new HashMap(3); + actionParams.put(PROP_DISPOSITION_ACTION_NAME, "destroy"); + actionParams.put(PROP_DISPOSITION_PERIOD, new Period("immediately|0")); + actionParams.put(PROP_DISPOSITION_PERIOD_PROPERTY, QName.createQName("{http://www.alfresco.org/model/recordsmanagement/1.0}cutOffDate")); + dispositionService.addDispositionActionDefinition(ds, actionParams); + + System.out.println("Created category '" + name); + + // Create the record folders + for (int i = 0; i < RECORD_FOLDER_COUNT; i++) + { + // Create the series + createRecordFolder(cat, i); + } + } + + private void createRecordFolder(NodeRef cat, int index) throws Exception + { + String name = genName("folder-", index); + Map properties = new HashMap(2); + properties.put(ContentModel.PROP_NAME, name); + properties.put(PROP_IDENTIFIER, identifierService.generateIdentifier(TYPE_RECORD_FOLDER, cat)); + NodeRef rf = nodeService.createNode( + cat, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, name), + TYPE_RECORD_FOLDER, + properties).getChildRef(); + + // Need to close the transaction and reopen to kick off required initialisation behaviour + userTransaction.commit(); + userTransaction = transactionService.getUserTransaction(); + userTransaction.begin(); + + System.out.println("Created record folder '" + name); + + // Create the records + for (int i = 0; i < RECORD_COUNT; i++) + { + createRecord(rf, i); + } + + userTransaction.commit(); + userTransaction = transactionService.getUserTransaction(); + userTransaction.begin(); + } + + private void createRecord(NodeRef recordFolder, int index) throws Exception + { + String name = genName("record-", index, ".txt"); + Map properties = new HashMap(2); + properties.put(ContentModel.PROP_NAME, name); + NodeRef r = nodeService.createNode( + recordFolder, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, name), + ContentModel.TYPE_CONTENT, + properties).getChildRef(); + ContentWriter cw = contentService.getWriter(r, ContentModel.PROP_CONTENT, true); + cw.setEncoding("UTF-8"); + cw.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + cw.putContent("This is my records content"); + + System.out.println("Created record '" + name); + } + + private String genName(String prefix, int index) + { + return genName(prefix, index, ""); + } + + private String genName(String prefix, int index, String postfix) + { + StringBuffer buff = new StringBuffer(120); + buff.append(prefix) + .append(index) + .append(postfix); + return buff.toString(); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/system/RecordsManagementServiceImplSystemTest.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/system/RecordsManagementServiceImplSystemTest.java new file mode 100644 index 0000000000..dd5f60c4d5 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/system/RecordsManagementServiceImplSystemTest.java @@ -0,0 +1,812 @@ +/* + * 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.module.org_alfresco_module_rm.test.system; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.transaction.UserTransaction; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService; +import org.alfresco.module.org_alfresco_module_rm.action.impl.BroadcastDispositionActionDefinitionUpdateAction; +import org.alfresco.module.org_alfresco_module_rm.action.impl.FileAction; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionAction; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionActionDefinition; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService; +import org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEvent; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.module.org_alfresco_module_rm.test.util.TestUtilities; +import org.alfresco.module.org_alfresco_module_rm.vital.VitalRecordDefinition; +import org.alfresco.module.org_alfresco_module_rm.vital.VitalRecordService; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.model.FileInfo; +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.Period; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.BaseSpringTest; + +/** + * System test for records management service. + * + * Awaiting refactoring into records management test. + * + * @author Roy Wetherall + */ +public class RecordsManagementServiceImplSystemTest extends BaseSpringTest implements RecordsManagementModel +{ + protected static StoreRef SPACES_STORE = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "SpacesStore"); + + private NodeRef filePlan; + + private FileFolderService fileFolderService; + private NodeService nodeService; + private NodeService unprotectedNodeService; + private RecordsManagementActionService rmActionService; + private RecordsManagementService rmService; + private SearchService searchService; + private TransactionService transactionService; + private RetryingTransactionHelper transactionHelper; + private DispositionService dispositionService; + private VitalRecordService vitalRecordService; + + @Override + protected void onSetUpInTransaction() throws Exception + { + super.onSetUpInTransaction(); + + // Get the service required in the tests + this.fileFolderService = (FileFolderService)this.applicationContext.getBean("FileFolderService"); + this.nodeService = (NodeService)this.applicationContext.getBean("NodeService"); + this.unprotectedNodeService = (NodeService)this.applicationContext.getBean("nodeService"); + this.transactionService = (TransactionService)this.applicationContext.getBean("TransactionService"); + this.searchService = (SearchService)this.applicationContext.getBean("searchService"); + this.rmActionService = (RecordsManagementActionService)this.applicationContext.getBean("recordsManagementActionService"); + this.rmService = (RecordsManagementService)this.applicationContext.getBean("recordsManagementService"); + this.transactionHelper = (RetryingTransactionHelper)this.applicationContext.getBean("retryingTransactionHelper"); + this.dispositionService = (DispositionService)this.applicationContext.getBean("dispositionService"); + vitalRecordService = (VitalRecordService)applicationContext.getBean("VitalRecordService"); + + // Set the current security context as admin + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + + // Get the test data + setUpTestData(); + } + + private void setUpTestData() + { + filePlan = TestUtilities.loadFilePlanData(applicationContext); + } + + @Override + protected void onTearDownInTransaction() throws Exception + { + try + { + UserTransaction txn = transactionService.getUserTransaction(false); + txn.begin(); + this.nodeService.deleteNode(filePlan); + txn.commit(); + } + catch (Exception e) + { + // Nothing + //System.out.println("DID NOT DELETE FILE PLAN!"); + } + } + + public void testDispositionPresence() throws Exception + { + setComplete(); + endTransaction(); + + // create a record category node in + final NodeRef nodeRef = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + NodeRef rootNode = nodeService.getRootNode(SPACES_STORE); + Map props = new HashMap(1); + String recordCategoryName = "Test Record Category"; + props.put(ContentModel.PROP_NAME, recordCategoryName); + NodeRef result = nodeService.createNode(rootNode, ContentModel.ASSOC_CHILDREN, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, QName.createValidLocalName(recordCategoryName)), + TYPE_RECORD_CATEGORY, props).getChildRef(); + return result; + } + }); + + + // ensure the record category node has the scheduled aspect and the disposition schedule association + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + assertTrue(nodeService.hasAspect(nodeRef, RecordsManagementModel.ASPECT_SCHEDULED)); + List scheduleAssocs = nodeService.getChildAssocs(nodeRef, ASSOC_DISPOSITION_SCHEDULE, RegexQNamePattern.MATCH_ALL); + + + assertNotNull(scheduleAssocs); + assertEquals(1, scheduleAssocs.size()); + + // test retrieval of the disposition schedule via RM service + DispositionSchedule schedule = dispositionService.getDispositionSchedule(nodeRef); + assertNotNull(schedule); + return null; + } + }); + } + + /** + * This test method contains a subset of the tests in TC 7-2 of the DoD doc. + * @throws Exception + */ + public void testRescheduleRecord_IsNotCutOff() throws Exception + { + final NodeRef recCat = TestUtilities.getRecordCategory(searchService, "Reports", "AIS Audit Records"); + // This RC has disposition instructions "Cut off monthly, hold 1 month, then destroy." + + setComplete(); + endTransaction(); + + // Create a suitable folder for this test. + final NodeRef testFolder = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + Map folderProps = new HashMap(1); + String folderName = "testFolder" + System.currentTimeMillis(); + folderProps.put(ContentModel.PROP_NAME, folderName); + NodeRef recordFolder = nodeService.createNode(recCat, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, folderName), + TYPE_RECORD_FOLDER).getChildRef(); + return recordFolder; + } + }); + + // Create a record in the test folder. File it and declare it. + final NodeRef testRecord = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + final NodeRef result = nodeService.createNode(testFolder, ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, + "Record" + System.currentTimeMillis() + ".txt"), + ContentModel.TYPE_CONTENT).getChildRef(); + + rmActionService.executeRecordsManagementAction(result, "file"); + TestUtilities.declareRecord(result, unprotectedNodeService, rmActionService); + return result; + } + }); + + assertTrue("recCat missing scheduled aspect", nodeService.hasAspect(recCat, RecordsManagementModel.ASPECT_SCHEDULED)); + assertFalse("folder should not have scheduled aspect", nodeService.hasAspect(testFolder, RecordsManagementModel.ASPECT_SCHEDULED)); + assertFalse("record should not have scheduled aspect", nodeService.hasAspect(testRecord, RecordsManagementModel.ASPECT_SCHEDULED)); + + assertFalse("recCat should not have dispositionLifecycle aspect", nodeService.hasAspect(recCat, RecordsManagementModel.ASPECT_DISPOSITION_LIFECYCLE)); + assertTrue("testFolder missing dispositionLifecycle aspect", nodeService.hasAspect(testFolder, RecordsManagementModel.ASPECT_DISPOSITION_LIFECYCLE)); + assertFalse("testRecord should not have dispositionLifecycle aspect", nodeService.hasAspect(testRecord, RecordsManagementModel.ASPECT_DISPOSITION_LIFECYCLE)); + + // Change the cutoff conditions for the associated record category + final Date dateBeforeChange = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Date execute() throws Throwable + { + Date asOfDate = dispositionService.getNextDispositionAction(testFolder).getAsOfDate(); + System.out.println("Going to change the disposition asOf Date."); + System.out.println(" - Original value: " + asOfDate); + + // Now change "Cut off monthly, hold 1 month, then destroy." + // to "Cut off yearly, hold 1 month, then destroy." + List dads = dispositionService.getDispositionSchedule(testFolder).getDispositionActionDefinitions(); + DispositionActionDefinition firstDAD = dads.get(0); + assertEquals("cutoff", firstDAD.getName()); + NodeRef dadNode = firstDAD.getNodeRef(); + + nodeService.setProperty(dadNode, PROP_DISPOSITION_PERIOD, new Period("year|1")); + + List updatedProps = new ArrayList(1); + updatedProps.add(PROP_DISPOSITION_PERIOD); + refreshDispositionActionDefinition(dadNode, updatedProps); + + return asOfDate; + } + }); + + // view the record metadata to verify that the record has been rescheduled. + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + DispositionAction nextDispositionAction = dispositionService.getNextDispositionAction(testFolder); + + assertEquals("cutoff", nextDispositionAction.getName()); + Date asOfDateAfterChange = nextDispositionAction.getAsOfDate(); + System.out.println(" - Updated value: " + asOfDateAfterChange); + + assertFalse("Expected disposition asOf date to change.", asOfDateAfterChange.equals(dateBeforeChange)); + return null; + } + }); + + // Change the disposition type (e.g. time-based to event-based) + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + List rmes = dispositionService.getNextDispositionAction(testFolder).getDispositionActionDefinition().getEvents(); + System.out.println("Going to change the RMEs."); + System.out.println(" - Original value: " + rmes); + + List dads = dispositionService.getDispositionSchedule(testFolder).getDispositionActionDefinitions(); + DispositionActionDefinition firstDAD = dads.get(0); + assertEquals("cutoff", firstDAD.getName()); + NodeRef dadNode = firstDAD.getNodeRef(); + +// nodeService.setProperty(dadNode, PROP_DISPOSITION_PERIOD, null); + List eventNames= new ArrayList(); + eventNames.add("study_complete"); + nodeService.setProperty(dadNode, PROP_DISPOSITION_EVENT, (Serializable)eventNames); + + return null; + } + }); + // Now add a second event to the same + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + DispositionAction nextDispositionAction = dispositionService.getNextDispositionAction(testFolder); + StringBuilder buf = new StringBuilder(); + for (RecordsManagementEvent e : nextDispositionAction.getDispositionActionDefinition().getEvents()) { + buf.append(e.getName()).append(','); + } + + System.out.println("Going to change the RMEs again."); + System.out.println(" - Original value: " + buf.toString()); + + List dads = dispositionService.getDispositionSchedule(testFolder).getDispositionActionDefinitions(); + DispositionActionDefinition firstDAD = dads.get(0); + assertEquals("cutoff", firstDAD.getName()); + NodeRef dadNode = firstDAD.getNodeRef(); + + List eventNames= new ArrayList(); + eventNames.add("study_complete"); + eventNames.add("case_complete"); + nodeService.setProperty(dadNode, PROP_DISPOSITION_EVENT, (Serializable)eventNames); + + return null; + } + }); + + // View the record metadata to verify that the record has been rescheduled. + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + DispositionAction nextDispositionAction = dispositionService.getNextDispositionAction(testFolder); + + assertEquals("cutoff", nextDispositionAction.getName()); + StringBuilder buf = new StringBuilder(); + for (RecordsManagementEvent e : nextDispositionAction.getDispositionActionDefinition().getEvents()) { + buf.append(e.getName()).append(','); + } + System.out.println(" - Updated value: " + buf.toString()); + + assertFalse("Disposition should not be eligible.", nextDispositionAction.isEventsEligible()); + return null; + } + }); + + // Tidy up test nodes. + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + nodeService.deleteNode(testRecord); + + // Change the disposition Period back to what it was. + List dads = dispositionService.getDispositionSchedule(testFolder).getDispositionActionDefinitions(); + DispositionActionDefinition firstDAD = dads.get(0); + assertEquals("cutoff", firstDAD.getName()); + NodeRef dadNode = firstDAD.getNodeRef(); + nodeService.setProperty(dadNode, PROP_DISPOSITION_PERIOD, new Period("month|1")); + + nodeService.deleteNode(testFolder); + + return null; + } + }); + } + + private void refreshDispositionActionDefinition(NodeRef nodeRef, List updatedProps) + { + if (updatedProps != null) + { + Map params = new HashMap(); + params.put(BroadcastDispositionActionDefinitionUpdateAction.CHANGED_PROPERTIES, (Serializable)updatedProps); + rmActionService.executeRecordsManagementAction(nodeRef, BroadcastDispositionActionDefinitionUpdateAction.NAME, params); + } + + // Remove the unpublished update aspect + nodeService.removeAspect(nodeRef, ASPECT_UNPUBLISHED_UPDATE); + } + + public void testGetDispositionInstructions() throws Exception + { + setComplete(); + endTransaction(); + + // Get a record + // TODO + + // Get a record folder + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + NodeRef folderRecord = TestUtilities.getRecordFolder(searchService, "Reports", "AIS Audit Records", "January AIS Audit Records"); + assertNotNull(folderRecord); + assertEquals("January AIS Audit Records", nodeService.getProperty(folderRecord, ContentModel.PROP_NAME)); + + assertFalse(rmService.isRecord(folderRecord)); + assertTrue(rmService.isRecordFolder(folderRecord)); + assertFalse(rmService.isRecordCategory(folderRecord)); + + DispositionSchedule di = dispositionService.getDispositionSchedule(folderRecord); + assertNotNull(di); + assertEquals("N1-218-00-4 item 023", di.getDispositionAuthority()); + assertEquals("Cut off monthly, hold 1 month, then destroy.", di.getDispositionInstructions()); + assertFalse(di.isRecordLevelDisposition()); + + // Get a record category + NodeRef recordCategory = TestUtilities.getRecordCategory(searchService, "Reports", "AIS Audit Records"); + assertNotNull(recordCategory); + assertEquals("AIS Audit Records", nodeService.getProperty(recordCategory, ContentModel.PROP_NAME)); + + assertFalse(rmService.isRecord(recordCategory)); + assertFalse(rmService.isRecordFolder(recordCategory)); + assertTrue(rmService.isRecordCategory(recordCategory)); + + di = dispositionService.getDispositionSchedule(recordCategory); + assertNotNull(di); + assertEquals("N1-218-00-4 item 023", di.getDispositionAuthority()); + assertEquals("Cut off monthly, hold 1 month, then destroy.", di.getDispositionInstructions()); + assertFalse(di.isRecordLevelDisposition()); + + List das = di.getDispositionActionDefinitions(); + assertNotNull(das); + assertEquals(2, das.size()); + assertEquals("cutoff", das.get(0).getName()); + assertEquals("destroy", das.get(1).getName()); + return null; + } + }); + } + + public void testMoveRecordWithinFileplan() + { + setComplete(); + endTransaction(); + + // We need record folders for test-filing as follows: + // 1. A 'clean' record folder with no disposition schedult and no review period. + // 2. A 'vital' record folder which has a review period defined. + // 3. A 'dispositionable' record folder which has an applicable disposition schedule. + // + // The example fileplan includes a folder which covers [2] and [3] together. + + final NodeRef cleanRecordFolder = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + NodeRef result = TestUtilities.getRecordFolder(searchService, "Civilian Files", "Case Files and Papers", "Gilbert Competency Hearing"); + assertNotNull("cleanRecordFolder was null", result); + + final DispositionSchedule dispositionSchedule = dispositionService.getDispositionSchedule(result); + assertNull("cleanRecordFolder had non-null disposition instructions.", dispositionSchedule.getDispositionInstructions()); + assertTrue("cleanRecordFolder had non-empty disposition instruction definitions.", dispositionSchedule.getDispositionActionDefinitions().isEmpty()); + + final VitalRecordDefinition vitalRecordDefinition = vitalRecordService.getVitalRecordDefinition(result); + assertEquals("cleanRecordFolder had wrong review period.", "0", vitalRecordDefinition.getReviewPeriod().getExpression()); + assertNull("cleanRecordFolder had non-null review date.", vitalRecordDefinition.getNextReviewDate()); + return result; + } + }); + final NodeRef dispAndVitalRecordFolder = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + NodeRef result = TestUtilities.getRecordFolder(searchService, "Reports", "AIS Audit Records", "January AIS Audit Records"); + assertNotNull("dispositionAndVitalRecordFolder was null", result); + + final DispositionSchedule dispositionSchedule = dispositionService.getDispositionSchedule(result); + assertNotNull("dispositionAndVitalRecordFolder had null disposition instructions.", dispositionSchedule.getDispositionInstructions()); + assertFalse("dispositionAndVitalRecordFolder had empty disposition instruction definitions.", dispositionSchedule.getDispositionActionDefinitions().isEmpty()); + + final VitalRecordDefinition vitalRecordDefinition = vitalRecordService.getVitalRecordDefinition(result); + assertFalse("dispositionAndVitalRecordFolder had wrong review period.", "none|0".equals(vitalRecordDefinition.getReviewPeriod().getExpression())); + assertNotNull("dispositionAndVitalRecordFolder had null review date.", vitalRecordDefinition.getNextReviewDate()); + return result; + } + }); + + // Create a record in the 'clean' folder. + final NodeRef testRecord = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + final NodeRef result = nodeService.createNode(cleanRecordFolder, ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, + "Record" + System.currentTimeMillis() + ".txt"), + ContentModel.TYPE_CONTENT).getChildRef(); + + rmActionService.executeRecordsManagementAction(result, "file"); + TestUtilities.declareRecord(result, unprotectedNodeService, rmActionService); + return result; + } + }); + + // Ensure it's devoid of all disposition and review-related state. + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + final DispositionSchedule dispositionSchedule = dispositionService.getDispositionSchedule(testRecord); + assertNull("testRecord had non-null disposition instructions.", dispositionSchedule.getDispositionInstructions()); + assertTrue("testRecord had non-empty disposition instruction definitions.", dispositionSchedule.getDispositionActionDefinitions().isEmpty()); + + final VitalRecordDefinition vitalRecordDefinition = vitalRecordService.getVitalRecordDefinition(testRecord); + assertEquals("testRecord had wrong review period.", "0", vitalRecordDefinition.getReviewPeriod().getExpression()); + assertNull("testRecord had non-null review date.", vitalRecordDefinition.getNextReviewDate()); + return null; + } + }); + + // Move from non-vital to vital - also non-dispositionable to dispositionable at the same time. + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + nodeService.moveNode(testRecord, dispAndVitalRecordFolder, ContentModel.ASSOC_CONTAINS, ContentModel.ASSOC_CONTAINS); + return null; + } + }); + + // Assert that the disposition and review-related data are correct after the move. + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + final DispositionSchedule dispositionSchedule = dispositionService.getDispositionSchedule(testRecord); + assertNotNull("testRecord had null disposition instructions.", dispositionSchedule.getDispositionInstructions()); + assertFalse("testRecord had empty disposition instruction definitions.", dispositionSchedule.getDispositionActionDefinitions().isEmpty()); + + final VitalRecordDefinition vitalRecordDefinition = vitalRecordService.getVitalRecordDefinition(testRecord); + assertFalse("testRecord had wrong review period.", "0".equals(vitalRecordDefinition.getReviewPeriod().getExpression())); + assertNotNull("testRecord had null review date.", vitalRecordDefinition.getNextReviewDate()); + return null; + } + }); + + // Move the test record back from vital to non-vital - also dispositionable to non-dispositionable at the same time. + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + nodeService.moveNode(testRecord, cleanRecordFolder, ContentModel.ASSOC_CONTAINS, ContentModel.ASSOC_CONTAINS); + return null; + } + }); + + // Assert that the disposition and review-related data are correct after the move. + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + final DispositionSchedule dispositionSchedule = dispositionService.getDispositionSchedule(testRecord); + assertNull("testRecord had non-null disposition instructions.", dispositionSchedule.getDispositionInstructions()); + assertTrue("testRecord had non-empty disposition instruction definitions.", dispositionSchedule.getDispositionActionDefinitions().isEmpty()); + + final VitalRecordDefinition vitalRecordDefinition = vitalRecordService.getVitalRecordDefinition(testRecord); + assertEquals("testRecord had wrong review period.", "0", vitalRecordDefinition.getReviewPeriod().getExpression()); + assertNull("testRecord had non-null review date.", vitalRecordDefinition.getNextReviewDate()); + return null; + } + }); + + //TODO check the search aspect + + // Tidy up. + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + nodeService.deleteNode(testRecord); + + return null; + } + }); + } + + public void testCopyRecordWithinFileplan() + { + setComplete(); + endTransaction(); + + // We need record folders for test-filing as follows: + // 1. A 'clean' record folder with no disposition schedule and no review period. + // 2. A 'vital' record folder which has a review period defined. + // 3. A 'dispositionable' record folder which has an applicable disposition schedule. + // + // The example fileplan includes a folder which covers [2] and [3] together. + + final NodeRef cleanRecordFolder = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + NodeRef result = TestUtilities.getRecordFolder(searchService, "Civilian Files", "Case Files and Papers", "Gilbert Competency Hearing"); + assertNotNull("cleanRecordFolder was null", result); + + final DispositionSchedule dispositionSchedule = dispositionService.getDispositionSchedule(result); + assertNull("cleanRecordFolder had non-null disposition instructions.", dispositionSchedule.getDispositionInstructions()); + assertTrue("cleanRecordFolder had non-empty disposition instruction definitions.", dispositionSchedule.getDispositionActionDefinitions().isEmpty()); + + final VitalRecordDefinition vitalRecordDefinition = vitalRecordService.getVitalRecordDefinition(result); + assertEquals("cleanRecordFolder had wrong review period.", "0", vitalRecordDefinition.getReviewPeriod().getExpression()); + assertNull("cleanRecordFolder had non-null review date.", vitalRecordDefinition.getNextReviewDate()); + return result; + } + }); + final NodeRef dispAndVitalRecordFolder = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + NodeRef result = TestUtilities.getRecordFolder(searchService, "Reports", "AIS Audit Records", "January AIS Audit Records"); + assertNotNull("dispositionAndVitalRecordFolder was null", result); + + final DispositionSchedule dispositionSchedule = dispositionService.getDispositionSchedule(result); + assertNotNull("dispositionAndVitalRecordFolder had null disposition instructions.", dispositionSchedule.getDispositionInstructions()); + assertFalse("dispositionAndVitalRecordFolder had empty disposition instruction definitions.", dispositionSchedule.getDispositionActionDefinitions().isEmpty()); + + final VitalRecordDefinition vitalRecordDefinition = vitalRecordService.getVitalRecordDefinition(result); + assertFalse("dispositionAndVitalRecordFolder had wrong review period.", "none|0".equals(vitalRecordDefinition.getReviewPeriod().getExpression())); + assertNotNull("dispositionAndVitalRecordFolder had null review date.", vitalRecordDefinition.getNextReviewDate()); + return result; + } + }); + + // Create a record in the 'clean' folder. + final NodeRef testRecord = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + final NodeRef result = nodeService.createNode(cleanRecordFolder, ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, + "Record" + System.currentTimeMillis() + ".txt"), + ContentModel.TYPE_CONTENT).getChildRef(); + + rmActionService.executeRecordsManagementAction(result, "file"); + TestUtilities.declareRecord(result, unprotectedNodeService, rmActionService); + return result; + } + }); + + // Ensure it's devoid of all disposition and review-related state. + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + final DispositionSchedule dispositionSchedule = dispositionService.getDispositionSchedule(testRecord); + assertNull("testRecord had non-null disposition instructions.", dispositionSchedule.getDispositionInstructions()); + assertTrue("testRecord had non-empty disposition instruction definitions.", dispositionSchedule.getDispositionActionDefinitions().isEmpty()); + + final VitalRecordDefinition vitalRecordDefinition = vitalRecordService.getVitalRecordDefinition(testRecord); + assertEquals("testRecord had wrong review period.", "0", vitalRecordDefinition.getReviewPeriod().getExpression()); + assertNull("testRecord had non-null review date.", vitalRecordDefinition.getNextReviewDate()); + return null; + } + }); + + // Copy from non-vital to vital - also non-dispositionable to dispositionable at the same time. + final NodeRef copiedNode = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + FileInfo fileInfo = fileFolderService.copy(testRecord, dispAndVitalRecordFolder, null); + NodeRef n = fileInfo.getNodeRef(); + return n; + } + }); + + // Assert that the disposition and review-related data are correct after the copy. + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + final DispositionSchedule dispositionSchedule = dispositionService.getDispositionSchedule(copiedNode); + assertNotNull("copiedNode had null disposition instructions.", dispositionSchedule.getDispositionInstructions()); + assertFalse("copiedNode had empty disposition instruction definitions.", dispositionSchedule.getDispositionActionDefinitions().isEmpty()); + + final VitalRecordDefinition vitalRecordDefinition = vitalRecordService.getVitalRecordDefinition(copiedNode); + assertFalse("copiedNode had wrong review period.", "0".equals(vitalRecordDefinition.getReviewPeriod().getExpression())); + assertNotNull("copiedNode had null review date.", vitalRecordDefinition.getNextReviewDate()); + return null; + } + }); + + // Create a record in the 'vital and disposition' folder. + final NodeRef testRecord2 = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + final NodeRef result = nodeService.createNode(dispAndVitalRecordFolder, ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, + "Record2" + System.currentTimeMillis() + ".txt"), + ContentModel.TYPE_CONTENT).getChildRef(); + + rmActionService.executeRecordsManagementAction(result, "file"); + TestUtilities.declareRecord(result, unprotectedNodeService, rmActionService); + return result; + } + }); + + // Check the vital and disposition status. + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + final DispositionSchedule dispositionSchedule = dispositionService.getDispositionSchedule(testRecord2); + assertNotNull("testRecord2 had null disposition instructions.", dispositionSchedule.getDispositionInstructions()); + assertFalse("testRecord2 had empty disposition instruction definitions.", dispositionSchedule.getDispositionActionDefinitions().isEmpty()); + + final VitalRecordDefinition vitalRecordDefinition = vitalRecordService.getVitalRecordDefinition(testRecord2); + assertFalse("testRecord2 had wrong review period.", "0".equals(vitalRecordDefinition.getReviewPeriod().getExpression())); + assertNotNull("testRecord2 had null review date.", vitalRecordDefinition.getNextReviewDate()); + return null; + } + }); + + // copy the record back from vital to non-vital - also dispositionable to non-dispositionable at the same time. + final NodeRef copiedBackNode = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + FileInfo fileInfo = fileFolderService.copy(testRecord2, cleanRecordFolder, null); // TODO Something wrong here. + NodeRef n = fileInfo.getNodeRef(); + return n; + } + }); + + // Assert that the disposition and review-related data are correct after the copy-back. + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + final DispositionSchedule dispositionSchedule = dispositionService.getDispositionSchedule(copiedBackNode); + assertNull("copiedBackNode had non-null disposition instructions.", dispositionSchedule.getDispositionInstructions()); + assertTrue("copiedBackNode had non-empty disposition instruction definitions.", dispositionSchedule.getDispositionActionDefinitions().isEmpty()); + + final VitalRecordDefinition vitalRecordDefinition = vitalRecordService.getVitalRecordDefinition(copiedBackNode); + assertEquals("copiedBackNode had wrong review period.", "0", vitalRecordDefinition.getReviewPeriod().getExpression()); + assertNull("copiedBackNode had non-null review date.", vitalRecordDefinition.getNextReviewDate()); + return null; + } + }); + + //TODO check the search aspect + + // Tidy up. + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + nodeService.deleteNode(copiedBackNode); + nodeService.deleteNode(testRecord2); + nodeService.deleteNode(copiedNode); + nodeService.deleteNode(testRecord); + + return null; + } + }); + } + + public void xxxtestUpdateNextDispositionAction() + { + setComplete(); + endTransaction(); + + final FileAction fileAction = (FileAction)applicationContext.getBean("file"); + + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + // Get a record folder + NodeRef folderRecord = TestUtilities.getRecordFolder(searchService, "Reports", "AIS Audit Records", "January AIS Audit Records"); + assertNotNull(folderRecord); + assertEquals("January AIS Audit Records", nodeService.getProperty(folderRecord, ContentModel.PROP_NAME)); + + DispositionSchedule di = dispositionService.getDispositionSchedule(folderRecord); + assertNotNull(di); + assertEquals("N1-218-00-4 item 023", di.getDispositionAuthority()); + assertEquals("Cut off monthly, hold 1 month, then destroy.", di.getDispositionInstructions()); + assertFalse(di.isRecordLevelDisposition()); + + assertFalse(nodeService.hasAspect(folderRecord, ASPECT_DISPOSITION_LIFECYCLE)); + + fileAction.updateNextDispositionAction(folderRecord); + + + // Check the next disposition action + assertTrue(nodeService.hasAspect(folderRecord, ASPECT_DISPOSITION_LIFECYCLE)); + NodeRef ndNodeRef = nodeService.getChildAssocs(folderRecord, ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + assertNotNull(ndNodeRef); + assertEquals("cutoff", nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_ACTION)); + assertEquals(di.getDispositionActionDefinitions().get(0).getId(), nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_ACTION_ID)); + assertNotNull(nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_AS_OF)); + + // Check the history is empty + // TODO + + Map props = new HashMap(1); + props.put(PROP_CUT_OFF_DATE, new Date()); + unprotectedNodeService.addAspect(folderRecord, ASPECT_CUT_OFF, props); + fileAction.updateNextDispositionAction(folderRecord); + + assertTrue(nodeService.hasAspect(folderRecord, ASPECT_DISPOSITION_LIFECYCLE)); + ndNodeRef = nodeService.getChildAssocs(folderRecord, ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); + assertNotNull(ndNodeRef); + assertEquals("destroy", nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_ACTION)); + assertEquals(di.getDispositionActionDefinitions().get(1).getId(), nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_ACTION_ID)); + assertNotNull(nodeService.getProperty(ndNodeRef, PROP_DISPOSITION_AS_OF)); + + // Check the history has an action + // TODO + + fileAction.updateNextDispositionAction(folderRecord); + + assertTrue(nodeService.hasAspect(folderRecord, ASPECT_DISPOSITION_LIFECYCLE)); + assertTrue(nodeService.getChildAssocs(folderRecord, ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).isEmpty()); + + // Check the history has both actions + // TODO + return null; + } + }); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/BaseRMTestCase.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/BaseRMTestCase.java new file mode 100644 index 0000000000..9037eca3fc --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/BaseRMTestCase.java @@ -0,0 +1,635 @@ +/* + * 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.module.org_alfresco_module_rm.test.util; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminService; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService; +import org.alfresco.module.org_alfresco_module_rm.action.impl.FreezeAction; +import org.alfresco.module.org_alfresco_module_rm.capability.CapabilityService; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService; +import org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEventService; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.module.org_alfresco_module_rm.model.RmSiteType; +import org.alfresco.module.org_alfresco_module_rm.search.RecordsManagementSearchService; +import org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService; +import org.alfresco.module.org_alfresco_module_rm.vital.VitalRecordService; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +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.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentWriter; +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.cmr.security.AuthorityService; +import org.alfresco.service.cmr.security.MutableAuthenticationService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.cmr.site.SiteInfo; +import org.alfresco.service.cmr.site.SiteService; +import org.alfresco.service.cmr.site.SiteVisibility; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.ApplicationContextHelper; +import org.alfresco.util.GUID; +import org.alfresco.util.RetryingTransactionHelperTestCase; +import org.springframework.context.ApplicationContext; + +/** + * Base test case class to use for RM unit tests. + * + * @author Roy Wetherall + */ +public abstract class BaseRMTestCase extends RetryingTransactionHelperTestCase + implements RecordsManagementModel, ContentModel +{ + /** Application context */ + protected static final String[] CONFIG_LOCATIONS = new String[] + { + "classpath:alfresco/application-context.xml", + "classpath:org/alfresco/module/org_alfresco_module_rm/test/util/test-context.xml" + }; + protected ApplicationContext applicationContext; + + /** Test model contants */ + protected String URI = "http://www.alfresco.org/model/rmtest/1.0"; + protected String PREFIX = "rmt"; + protected QName TYPE_CUSTOM_TYPE = QName.createQName(URI, "customType"); + protected QName ASPECT_CUSTOM_ASPECT = QName.createQName(URI, "customAspect"); + protected QName ASPECT_RECORD_META_DATA = QName.createQName(URI, "recordMetaData"); + + /** Site id */ + protected static final String SITE_ID = "mySite"; + + /** Services */ + protected NodeService nodeService; + protected ContentService contentService; + protected DictionaryService dictionaryService; + protected RetryingTransactionHelper retryingTransactionHelper; + protected PolicyComponent policyComponent; + protected NamespaceService namespaceService; + protected SearchService searchService; + protected SiteService siteService; + protected MutableAuthenticationService authenticationService; + protected AuthorityService authorityService; + protected PersonService personService; + + /** RM Services */ + protected RecordsManagementService rmService; + protected DispositionService dispositionService; + protected RecordsManagementEventService eventService; + protected RecordsManagementAdminService adminService; + protected RecordsManagementActionService actionService; + protected RecordsManagementSearchService rmSearchService; + protected RecordsManagementSecurityService securityService; + protected CapabilityService capabilityService; + protected VitalRecordService vitalRecordService; + + /** test data */ + protected StoreRef storeRef; + protected NodeRef rootNodeRef; + protected SiteInfo siteInfo; + protected NodeRef folder; + protected NodeRef filePlan; + protected NodeRef rmContainer; + protected DispositionSchedule dispositionSchedule; + protected NodeRef rmFolder; + + /** multi-hierarchy test data + * + * |--rmRootContainer + * | + * |--mhContainer + * | + * |--mhContainer-1-1 (has schedule - folder level) + * | | + * | |--mhContainer-2-1 + * | | + * | |--mhContainer-3-1 + * | + * |--mhContainer-1-2 (has schedule - folder level) + * | + * |--mhContainer-2-2 + * | | + * | |--mhContainer-3-2 + * | | + * | |--mhContainer-3-3 (has schedule - record level) + * | + * |--mhContainer-2-3 (has schedule - folder level) + * | + * |--mhContainer-3-4 + * | + * |--mhContainer-3-5 (has schedule- record level) + */ + + protected NodeRef mhContainer; + + protected NodeRef mhContainer11; + protected DispositionSchedule mhDispositionSchedule11; + protected NodeRef mhContainer12; + protected DispositionSchedule mhDispositionSchedule12; + + protected NodeRef mhContainer21; + protected NodeRef mhContainer22; + protected NodeRef mhContainer23; + protected DispositionSchedule mhDispositionSchedule23; + + protected NodeRef mhContainer31; + protected NodeRef mhContainer32; + protected NodeRef mhContainer33; + protected DispositionSchedule mhDispositionSchedule33; + protected NodeRef mhContainer34; + protected NodeRef mhContainer35; + protected DispositionSchedule mhDispositionSchedule35; + + protected NodeRef mhRecordFolder41; + protected NodeRef mhRecordFolder42; + protected NodeRef mhRecordFolder43; + protected NodeRef mhRecordFolder44; + protected NodeRef mhRecordFolder45; + + /** test user names */ + protected String[] testUsers; + protected String userName; + protected String rmUserName; + protected String powerUserName; + protected String securityOfficerName; + protected String recordsManagerName; + protected String rmAdminName; + + /** test people */ + protected NodeRef userPerson; + protected NodeRef rmUserPerson; + protected NodeRef powerUserPerson; + protected NodeRef securityOfficerPerson; + protected NodeRef recordsManagerPerson; + protected NodeRef rmAdminPerson; + + /** test values */ + protected static final String DEFAULT_DISPOSITION_AUTHORITY = "disposition authority"; + protected static final String DEFAULT_DISPOSITION_INSTRUCTIONS = "disposition instructions"; + protected static final String DEFAULT_DISPOSITION_DESCRIPTION = "disposition action description"; + protected static final String DEFAULT_EVENT_NAME = "case_closed"; + protected static final String PERIOD_NONE = "none|0"; + + /** + * Indicates whether this is a multi-hierarchy test or not. If it is then the multi-hierarchy record + * taxonomy test data is loaded. + */ + protected boolean isMultiHierarchyTest() + { + return false; + } + + /** + * Indicates whether the test users should be created or not. + * @return + */ + protected boolean isUserTest() + { + return false; + } + + /** + * @see junit.framework.TestCase#setUp() + */ + @Override + protected void setUp() throws Exception + { + // Get the application context + applicationContext = ApplicationContextHelper.getApplicationContext(CONFIG_LOCATIONS); + + // Initialise the service beans + initServices(); + + // Setup test data + setupTestData(); + if (isMultiHierarchyTest() == true) + { + setupMultiHierarchyTestData(); + } + // Create the users here + if (isUserTest() == true) + { + setupTestUsers(filePlan); + } + } + + /** + * Initialise the service beans. + */ + protected void initServices() + { + // Get services + nodeService = (NodeService)applicationContext.getBean("NodeService"); + contentService = (ContentService)applicationContext.getBean("ContentService"); + retryingTransactionHelper = (RetryingTransactionHelper)applicationContext.getBean("retryingTransactionHelper"); + namespaceService = (NamespaceService)this.applicationContext.getBean("NamespaceService"); + searchService = (SearchService)this.applicationContext.getBean("SearchService"); + policyComponent = (PolicyComponent)this.applicationContext.getBean("policyComponent"); + dictionaryService = (DictionaryService)this.applicationContext.getBean("DictionaryService"); + siteService = (SiteService)this.applicationContext.getBean("SiteService"); + authorityService = (AuthorityService)this.applicationContext.getBean("AuthorityService"); + authenticationService = (MutableAuthenticationService)this.applicationContext.getBean("AuthenticationService"); + personService = (PersonService)this.applicationContext.getBean("PersonService"); + + // Get RM services + rmService = (RecordsManagementService)applicationContext.getBean("RecordsManagementService"); + dispositionService = (DispositionService)applicationContext.getBean("DispositionService"); + eventService = (RecordsManagementEventService)applicationContext.getBean("RecordsManagementEventService"); + adminService = (RecordsManagementAdminService)applicationContext.getBean("RecordsManagementAdminService"); + actionService = (RecordsManagementActionService)this.applicationContext.getBean("RecordsManagementActionService"); + rmSearchService = (RecordsManagementSearchService)this.applicationContext.getBean("RecordsManagementSearchService"); + securityService = (RecordsManagementSecurityService)this.applicationContext.getBean("RecordsManagementSecurityService"); + capabilityService = (CapabilityService)this.applicationContext.getBean("CapabilityService"); + vitalRecordService = (VitalRecordService)this.applicationContext.getBean("VitalRecordService"); + } + + /** + * @see junit.framework.TestCase#tearDown() + */ + @Override + protected void tearDown() throws Exception + { + retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + // As system user + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + + // Do the tear down + tearDownImpl(); + + return null; + } + }); + } + + /** + * Tear down implementation + */ + protected void tearDownImpl() + { + // Delete the folder + nodeService.deleteNode(folder); + + // Delete the site + siteService.deleteSite(SITE_ID); + } + + /** + * @see org.alfresco.util.RetryingTransactionHelperTestCase#getRetryingTransactionHelper() + */ + @Override + public RetryingTransactionHelper getRetryingTransactionHelper() + { + return retryingTransactionHelper; + } + + /** + * Setup test data for tests + */ + protected void setupTestData() + { + retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + setupTestDataImpl(); + return null; + } + }); + } + + /** + * Impl of test data setup + */ + protected void setupTestDataImpl() + { + storeRef = StoreRef.STORE_REF_WORKSPACE_SPACESSTORE; + rootNodeRef = nodeService.getRootNode(storeRef); + + // Create folder + String containerName = "RM2_" + System.currentTimeMillis(); + Map containerProps = new HashMap(1); + containerProps.put(ContentModel.PROP_NAME, containerName); + folder = nodeService.createNode( + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, containerName), + ContentModel.TYPE_FOLDER, + containerProps).getChildRef(); + assertNotNull("Could not create base folder", folder); + + // Create the site + siteInfo = siteService.createSite("preset", SITE_ID, "title", "descrition", SiteVisibility.PUBLIC, RecordsManagementModel.TYPE_RM_SITE); + filePlan = siteService.getContainer(SITE_ID, RmSiteType.COMPONENT_DOCUMENT_LIBRARY); + assertNotNull("Site document library container was not created successfully.", filePlan); + + // Create RM container + rmContainer = rmService.createRecordCategory(filePlan, "rmContainer"); + assertNotNull("Could not create rm container", rmContainer); + + // Create disposition schedule + dispositionSchedule = createBasicDispositionSchedule(rmContainer); + + // Create RM folder + rmFolder = rmService.createRecordFolder(rmContainer, "rmFolder"); + assertNotNull("Could not create rm folder", rmFolder); + } + + protected void setupTestUsers(final NodeRef filePlan) + { + retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + setupTestUsersImpl(filePlan); + return null; + } + }); + } + + /** + * + * @param filePlan + */ + protected void setupTestUsersImpl(NodeRef filePlan) + { + userName = GUID.generate(); + userPerson = createPerson(userName); + + rmUserName = GUID.generate(); + rmUserPerson = createPerson(rmUserName); + securityService.assignRoleToAuthority(filePlan, "User", rmUserName); + + powerUserName = GUID.generate(); + powerUserPerson = createPerson(powerUserName); + securityService.assignRoleToAuthority(filePlan, "PowerUser", powerUserName); + + securityOfficerName = GUID.generate(); + securityOfficerPerson = createPerson(securityOfficerName); + securityService.assignRoleToAuthority(filePlan, "SecurityOfficer", securityOfficerName); + + recordsManagerName = GUID.generate(); + recordsManagerPerson = createPerson(recordsManagerName); + securityService.assignRoleToAuthority(filePlan, "RecordsManager", recordsManagerName); + + rmAdminName = GUID.generate(); + rmAdminPerson = createPerson(rmAdminName); + securityService.assignRoleToAuthority(filePlan, "Administrator", rmAdminName); + + testUsers = new String[] + { + userName, + rmUserName, + powerUserName, + securityOfficerName, + recordsManagerName, + rmAdminName + }; + } + + protected NodeRef createPerson(String userName) + { + authenticationService.createAuthentication(userName, "password".toCharArray()); + Map properties = new HashMap(); + properties.put(ContentModel.PROP_USERNAME, userName); + return personService.createPerson(properties); + } + + /** + * Setup multi hierarchy test data + */ + protected void setupMultiHierarchyTestData() + { + retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + // As system user + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + + // Do setup + setupMultiHierarchyTestDataImpl(); + + return null; + } + }); + } + + /** + * Impl of multi hierarchy test data + */ + protected void setupMultiHierarchyTestDataImpl() + { + // Create root mh container + mhContainer = rmService.createRecordCategory(filePlan, "mhContainer"); + + // Level 1 + mhContainer11 = rmService.createRecordCategory(mhContainer, "mhContainer11"); + mhDispositionSchedule11 = createBasicDispositionSchedule(mhContainer11, "ds11", DEFAULT_DISPOSITION_AUTHORITY, false, true); + mhContainer12 = rmService.createRecordCategory(mhContainer, "mhContainer12"); + mhDispositionSchedule12 = createBasicDispositionSchedule(mhContainer12, "ds12", DEFAULT_DISPOSITION_AUTHORITY, false, true); + + // Level 2 + mhContainer21 = rmService.createRecordCategory(mhContainer11, "mhContainer21"); + mhContainer22 = rmService.createRecordCategory(mhContainer12, "mhContainer22"); + mhContainer23 = rmService.createRecordCategory(mhContainer12, "mhContainer23"); + mhDispositionSchedule23 = createBasicDispositionSchedule(mhContainer23, "ds23", DEFAULT_DISPOSITION_AUTHORITY, false, true); + + // Level 3 + mhContainer31 = rmService.createRecordCategory(mhContainer21, "mhContainer31"); + mhContainer32 = rmService.createRecordCategory(mhContainer22, "mhContainer32"); + mhContainer33 = rmService.createRecordCategory(mhContainer22, "mhContainer33"); + mhDispositionSchedule33 = createBasicDispositionSchedule(mhContainer33, "ds33", DEFAULT_DISPOSITION_AUTHORITY, true, true); + mhContainer34 = rmService.createRecordCategory(mhContainer23, "mhContainer34"); + mhContainer35 = rmService.createRecordCategory(mhContainer23, "mhContainer35"); + mhDispositionSchedule35 = createBasicDispositionSchedule(mhContainer35, "ds35", DEFAULT_DISPOSITION_AUTHORITY, true, true); + + // Record folders + mhRecordFolder41 = rmService.createRecordFolder(mhContainer31, "mhFolder41"); + mhRecordFolder42 = rmService.createRecordFolder(mhContainer32, "mhFolder42"); + mhRecordFolder43 = rmService.createRecordFolder(mhContainer33, "mhFolder43"); + mhRecordFolder44 = rmService.createRecordFolder(mhContainer34, "mhFolder44"); + mhRecordFolder45 = rmService.createRecordFolder(mhContainer35, "mhFolder45"); + } + + /** + * + * @param container + * @return + */ + protected DispositionSchedule createBasicDispositionSchedule(NodeRef container) + { + return createBasicDispositionSchedule(container, DEFAULT_DISPOSITION_INSTRUCTIONS, DEFAULT_DISPOSITION_AUTHORITY, false, true); + } + + /** + * + * @param container + * @param isRecordLevel + * @param defaultDispositionActions + * @return + */ + protected DispositionSchedule createBasicDispositionSchedule( + NodeRef container, + String dispositionInstructions, + String dispositionAuthority, + boolean isRecordLevel, + boolean defaultDispositionActions) + { + Map dsProps = new HashMap(3); + dsProps.put(PROP_DISPOSITION_AUTHORITY, dispositionAuthority); + dsProps.put(PROP_DISPOSITION_INSTRUCTIONS, dispositionInstructions); + dsProps.put(PROP_RECORD_LEVEL_DISPOSITION, isRecordLevel); + DispositionSchedule dispositionSchedule = dispositionService.createDispositionSchedule(container, dsProps); + assertNotNull(dispositionSchedule); + + if (defaultDispositionActions == true) + { + Map adParams = new HashMap(3); + adParams.put(PROP_DISPOSITION_ACTION_NAME, "cutoff"); + adParams.put(PROP_DISPOSITION_DESCRIPTION, DEFAULT_DISPOSITION_DESCRIPTION); + + List events = new ArrayList(1); + events.add(DEFAULT_EVENT_NAME); + adParams.put(PROP_DISPOSITION_EVENT, (Serializable)events); + + dispositionService.addDispositionActionDefinition(dispositionSchedule, adParams); + + adParams = new HashMap(3); + adParams.put(PROP_DISPOSITION_ACTION_NAME, "destroy"); + adParams.put(PROP_DISPOSITION_DESCRIPTION, DEFAULT_DISPOSITION_DESCRIPTION); + adParams.put(PROP_DISPOSITION_PERIOD, "immediately|0"); + + dispositionService.addDispositionActionDefinition(dispositionSchedule, adParams); + } + + return dispositionSchedule; + } + + protected NodeRef createRecord(NodeRef recordFolder, String name) + { + return createRecord(recordFolder, name, null, "Some test content"); + } + + protected NodeRef createRecord(NodeRef recordFolder, String name, Map properties, String content) + { + // Create the document + if (properties == null) + { + properties = new HashMap(1); + } + if (properties.containsKey(ContentModel.PROP_NAME) == false) + { + properties.put(ContentModel.PROP_NAME, name); + } + NodeRef recordOne = this.nodeService.createNode(recordFolder, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, name), + ContentModel.TYPE_CONTENT, + properties).getChildRef(); + + // Set the content + ContentWriter writer = contentService.getWriter(recordOne, ContentModel.PROP_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.setEncoding("UTF-8"); + writer.putContent(content); + + return recordOne; + } + + protected void declareRecord(final NodeRef record) + { + AuthenticationUtil.runAs(new RunAsWork() + { + @Override + public Void doWork() throws Exception + { + // Declare record + nodeService.setProperty(record, RecordsManagementModel.PROP_PUBLICATION_DATE, new Date()); + nodeService.setProperty(record, RecordsManagementModel.PROP_MEDIA_TYPE, "mediaTypeValue"); + nodeService.setProperty(record, RecordsManagementModel.PROP_FORMAT, "formatValue"); + nodeService.setProperty(record, RecordsManagementModel.PROP_DATE_RECEIVED, new Date()); + nodeService.setProperty(record, RecordsManagementModel.PROP_DATE_FILED, new Date()); + nodeService.setProperty(record, RecordsManagementModel.PROP_ORIGINATOR, "origValue"); + nodeService.setProperty(record, RecordsManagementModel.PROP_ORIGINATING_ORGANIZATION, "origOrgValue"); + nodeService.setProperty(record, ContentModel.PROP_TITLE, "titleValue"); + actionService.executeRecordsManagementAction(record, "declareRecord"); + + return null; + } + + }, AuthenticationUtil.getAdminUserName()); + + } + + protected void freeze(final NodeRef nodeRef) + { + AuthenticationUtil.runAs(new RunAsWork() + { + @Override + public Void doWork() throws Exception + { + Map params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "Freeze reason."); + actionService.executeRecordsManagementAction(nodeRef, "freeze", params); + + return null; + } + + }, AuthenticationUtil.getSystemUserName()); + } + + protected void unfreeze(final NodeRef nodeRef) + { + AuthenticationUtil.runAs(new RunAsWork() + { + @Override + public Void doWork() throws Exception + { + actionService.executeRecordsManagementAction(nodeRef, "unfreeze"); + return null; + } + + }, AuthenticationUtil.getSystemUserName()); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestAction.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestAction.java new file mode 100644 index 0000000000..aeb4ecccdc --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestAction.java @@ -0,0 +1,57 @@ +/* + * 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.module.org_alfresco_module_rm.test.util; + +import java.io.Serializable; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.action.RMActionExecuterAbstractBase; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.repository.NodeRef; + +public class TestAction extends RMActionExecuterAbstractBase +{ + public static final String NAME = "testAction"; + public static final String PARAM = "testActionParam"; + public static final String PARAM_VALUE = "value"; + + @Override + protected void executeImpl(Action action, NodeRef actionedUponNodeRef) + { + if (action.getParameterValue(PARAM).equals(PARAM_VALUE) == false) + { + throw new RuntimeException("Unexpected parameter value. Expected " + PARAM_VALUE + " actual " + action.getParameterValue(PARAM)); + } + this.nodeService.addAspect(actionedUponNodeRef, ASPECT_RECORD, null); + } + + @Override + public boolean isDispositionAction() + { + return true; + } + + @Override + protected boolean isExecutableImpl(NodeRef filePlanComponent, Map parameters, boolean throwException) + { + return true; + } + + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestAction2.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestAction2.java new file mode 100644 index 0000000000..7143c78477 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestAction2.java @@ -0,0 +1,51 @@ +/* + * 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.module.org_alfresco_module_rm.test.util; + +import java.io.Serializable; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.action.RMActionExecuterAbstractBase; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.repository.NodeRef; + +public class TestAction2 extends RMActionExecuterAbstractBase +{ + public static final String NAME = "testAction2"; + + @Override + protected void executeImpl(Action action, NodeRef actionedUponNodeRef) + { + // Do nothing + } + + @Override + public boolean isDispositionAction() + { + return false; + } + + @Override + protected boolean isExecutableImpl(NodeRef filePlanComponent, Map parameters, boolean throwException) + { + return true; + } + + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestActionParams.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestActionParams.java new file mode 100644 index 0000000000..8bcca2f956 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestActionParams.java @@ -0,0 +1,52 @@ +/* + * 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.module.org_alfresco_module_rm.test.util; + +import java.io.Serializable; +import java.util.Map; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.module.org_alfresco_module_rm.action.RMActionExecuterAbstractBase; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.repository.NodeRef; + +public class TestActionParams extends RMActionExecuterAbstractBase +{ + public static final String NAME = "testActionParams"; + public static final String PARAM_DATE = "paramDate"; + + @Override + protected void executeImpl(Action action, NodeRef actionedUponNodeRef) + { + Object dateValue = action.getParameterValue(PARAM_DATE); + if ((dateValue instanceof java.util.Date) == false) + { + throw new AlfrescoRuntimeException("Param we not a Date as expected."); + } + } + + /* (non-Javadoc) + * @see org.alfresco.module.org_alfresco_module_rm.action.RMActionExecuterAbstractBase#isExecutableImpl(org.alfresco.service.cmr.repository.NodeRef, java.util.Map, boolean) + */ + @Override + protected boolean isExecutableImpl(NodeRef filePlanComponent, Map parameters, boolean throwException) + { + return true; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestUtilities.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestUtilities.java new file mode 100644 index 0000000000..b579549521 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestUtilities.java @@ -0,0 +1,238 @@ +/* + * 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.module.org_alfresco_module_rm.test.util; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import junit.framework.Assert; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService; +import org.alfresco.module.org_alfresco_module_rm.dod5015.DOD5015Model; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementSearchBehaviour; +import org.alfresco.module.org_alfresco_module_rm.script.BootstrapTestDataGet; +import org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService; +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.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.SearchParameters; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.cmr.security.AuthorityService; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.cmr.view.ImporterBinding; +import org.alfresco.service.cmr.view.ImporterService; +import org.alfresco.service.cmr.view.Location; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.ISO9075; +import org.springframework.context.ApplicationContext; + +/** + * This class is an initial placeholder for miscellaneous helper methods used in + * the testing or test initialisation of the DOD5015 module. + * + * @author neilm + */ +public class TestUtilities implements RecordsManagementModel +{ + public static NodeRef loadFilePlanData(ApplicationContext applicationContext) + { + return TestUtilities.loadFilePlanData(applicationContext, true, false); + } + + public static NodeRef loadFilePlanData(ApplicationContext applicationContext, boolean patchData, boolean alwaysLoad) + { + NodeService nodeService = (NodeService)applicationContext.getBean("NodeService"); + AuthorityService authorityService = (AuthorityService)applicationContext.getBean("AuthorityService"); + PermissionService permissionService = (PermissionService)applicationContext.getBean("PermissionService"); + SearchService searchService = (SearchService)applicationContext.getBean("SearchService"); + ImporterService importerService = (ImporterService)applicationContext.getBean("importerComponent"); + RecordsManagementService recordsManagementService = (RecordsManagementService)applicationContext.getBean("RecordsManagementService"); + RecordsManagementActionService recordsManagementActionService = (RecordsManagementActionService)applicationContext.getBean("RecordsManagementActionService"); + RecordsManagementSecurityService recordsManagementSecurityService = (RecordsManagementSecurityService)applicationContext.getBean("RecordsManagementSecurityService"); + RecordsManagementSearchBehaviour recordsManagementSearchBehaviour = (RecordsManagementSearchBehaviour)applicationContext.getBean("recordsManagementSearchBehaviour"); + DispositionService dispositionService = (DispositionService)applicationContext.getBean("DispositionService"); + + NodeRef filePlan = null; + NodeRef rootNode = nodeService.getRootNode(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE); + + if (alwaysLoad == false) + { + // Try and find a file plan hanging from the root node + List assocs = nodeService.getChildAssocs(rootNode, ContentModel.ASSOC_CHILDREN, TYPE_FILE_PLAN); + if (assocs.size() != 0) + { + filePlan = assocs.get(0).getChildRef(); + return filePlan; + } + } + + // For now creating the filePlan beneath the + filePlan = nodeService.createNode(rootNode, ContentModel.ASSOC_CHILDREN, + TYPE_FILE_PLAN, + TYPE_FILE_PLAN).getChildRef(); + + // Do the data load into the the provided filePlan node reference + // TODO ... + InputStream is = TestUtilities.class.getClassLoader().getResourceAsStream( + "alfresco/module/org_alfresco_module_rm/bootstrap/DODExampleFilePlan.xml"); + //"alfresco/module/org_alfresco_module_rm/bootstrap/temp.xml"); + Assert.assertNotNull("The DODExampleFilePlan.xml import file could not be found", is); + Reader viewReader = new InputStreamReader(is); + Location location = new Location(filePlan); + importerService.importView(viewReader, location, REPLACE_BINDING, null); + + if (patchData == true) + { + // Tempory call out to patch data after AMP + BootstrapTestDataGet.patchLoadedData(searchService, nodeService, recordsManagementService, + recordsManagementActionService, permissionService, + authorityService, recordsManagementSecurityService, + recordsManagementSearchBehaviour, + dispositionService); + } + + return filePlan; + } + + public static NodeRef getRecordSeries(SearchService searchService, String seriesName) + { + SearchParameters searchParameters = new SearchParameters(); + searchParameters.addStore(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE); + + String query = "PATH:\"dod:filePlan/cm:" + ISO9075.encode(seriesName) + "\""; + + searchParameters.setQuery(query); + searchParameters.setLanguage(SearchService.LANGUAGE_LUCENE); + ResultSet rs = searchService.query(searchParameters); + try + { + //setComplete(); + //endTransaction(); + return rs.getNodeRefs().isEmpty() ? null : rs.getNodeRef(0); + } + finally + { + rs.close(); + } + } + + public static NodeRef getRecordCategory(SearchService searchService, String seriesName, String categoryName) + { + SearchParameters searchParameters = new SearchParameters(); + searchParameters.addStore(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE); + + String query = "PATH:\"dod:filePlan/cm:" + ISO9075.encode(seriesName) + "/cm:" + ISO9075.encode(categoryName) + "\""; + + searchParameters.setQuery(query); + searchParameters.setLanguage(SearchService.LANGUAGE_LUCENE); + ResultSet rs = searchService.query(searchParameters); + try + { + //setComplete(); + //endTransaction(); + return rs.getNodeRefs().isEmpty() ? null : rs.getNodeRef(0); + } + finally + { + rs.close(); + } + } + + public static NodeRef getRecordFolder(SearchService searchService, String seriesName, String categoryName, String folderName) + { + SearchParameters searchParameters = new SearchParameters(); + searchParameters.addStore(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE); + String query = "PATH:\"dod:filePlan/cm:" + ISO9075.encode(seriesName) + + "/cm:" + ISO9075.encode(categoryName) + + "/cm:" + ISO9075.encode(folderName) + "\""; + System.out.println("Query: " + query); + searchParameters.setQuery(query); + searchParameters.setLanguage(SearchService.LANGUAGE_LUCENE); + ResultSet rs = searchService.query(searchParameters); + try + { + // setComplete(); + // endTransaction(); + return rs.getNodeRefs().isEmpty() ? null : rs.getNodeRef(0); + } + finally + { + rs.close(); + } + } + + + // TODO .. do we need to redeclare this here ?? + private static ImporterBinding REPLACE_BINDING = new ImporterBinding() + { + + public UUID_BINDING getUUIDBinding() + { + return UUID_BINDING.REPLACE_EXISTING; + } + + public String getValue(String key) + { + return null; + } + + public boolean allowReferenceWithinTransaction() + { + return false; + } + + public QName[] getExcludedClasses() + { + return null; + } + + }; + + public static void declareRecord(NodeRef recordToDeclare, NodeService nodeService, + RecordsManagementActionService rmActionService) + { + // Declare record + Map propValues = nodeService.getProperties(recordToDeclare); + propValues.put(RecordsManagementModel.PROP_PUBLICATION_DATE, new Date()); + List smList = new ArrayList(2); +// smList.add(DOD5015Test.FOUO); +// smList.add(DOD5015Test.NOFORN); + propValues.put(RecordsManagementModel.PROP_SUPPLEMENTAL_MARKING_LIST, (Serializable)smList); + propValues.put(RecordsManagementModel.PROP_MEDIA_TYPE, "mediaTypeValue"); + propValues.put(RecordsManagementModel.PROP_FORMAT, "formatValue"); + propValues.put(RecordsManagementModel.PROP_DATE_RECEIVED, new Date()); + propValues.put(RecordsManagementModel.PROP_ORIGINATOR, "origValue"); + propValues.put(RecordsManagementModel.PROP_ORIGINATING_ORGANIZATION, "origOrgValue"); + propValues.put(ContentModel.PROP_TITLE, "titleValue"); + nodeService.setProperties(recordToDeclare, propValues); + rmActionService.executeRecordsManagementAction(recordToDeclare, "declareRecord"); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestWebScriptRepoServer.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestWebScriptRepoServer.java new file mode 100644 index 0000000000..57b1748332 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestWebScriptRepoServer.java @@ -0,0 +1,221 @@ +/* + * 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.module.org_alfresco_module_rm.test.util; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; + +import org.alfresco.repo.security.authentication.AuthenticationException; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.util.EqualsHelper; +import org.springframework.extensions.webscripts.TestWebScriptServer; +import org.springframework.context.support.ClassPathXmlApplicationContext; + + +/** + * Stand-alone Web Script Test Server + * + * @author davidc + */ +public class TestWebScriptRepoServer extends TestWebScriptServer +{ + /** + * Main entry point. + */ + public static void main(String[] args) + { + try + { + TestWebScriptServer testServer = getTestServer(); + AuthenticationUtil.setRunAsUserSystem(); + testServer.rep(); + } + catch(Throwable e) + { + StringWriter strWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(strWriter); + e.printStackTrace(printWriter); + System.out.println(strWriter.toString()); + } + finally + { + System.exit(0); + } + } + + private final static String[] CONFIG_LOCATIONS = new String[] + { + "classpath:alfresco/application-context.xml", + "classpath:alfresco/web-scripts-application-context.xml", + "classpath:alfresco/web-scripts-application-context-test.xml" + }; + + /** A static reference to the application context being used */ + private static ClassPathXmlApplicationContext ctx; + private static String appendedTestConfiguration; + + private RetryingTransactionHelper retryingTransactionHelper; + private AuthenticationService authenticationService; + + + /** + * Sets helper that provides transaction callbacks + */ + public void setTransactionHelper(RetryingTransactionHelper retryingTransactionHelper) + { + this.retryingTransactionHelper = retryingTransactionHelper; + } + + /** + * @param authenticationService + */ + public void setAuthenticationService(AuthenticationService authenticationService) + { + this.authenticationService = authenticationService; + } + + /** + * Get default user name + */ + protected String getDefaultUserName() + { + return AuthenticationUtil.getAdminUserName(); + } + + /** + * {@inheritDoc #getTestServer(String)} + */ + public static TestWebScriptServer getTestServer() + { + return getTestServer(null); + } + + /** + * Start up a context and get the server bean. + *

+ * This method will close and restart the application context only if the configuration has + * changed. + * + * @param appendTestConfigLocation additional context file to include in the application context + * @return Test Server + */ + public static synchronized TestWebScriptServer getTestServer(String appendTestConfigLocation) + { + if (TestWebScriptRepoServer.ctx != null) + { + boolean configChanged = !EqualsHelper.nullSafeEquals( + appendTestConfigLocation, + TestWebScriptRepoServer.appendedTestConfiguration); + if (configChanged) + { + // The config changed, so close the context (it'll be restarted later) + try + { + ctx.close(); + ctx = null; + } + catch (Throwable e) + { + throw new RuntimeException("Failed to shut down existing application context", e); + } + } + else + { + // There is already a context with the required configuration + } + } + + // Check if we need to start/restart the context + if (TestWebScriptRepoServer.ctx == null) + { + // Restart it + final String[] configLocations; + if (appendTestConfigLocation == null) + { + configLocations = CONFIG_LOCATIONS; + } + else + { + configLocations = new String[CONFIG_LOCATIONS.length+1]; + System.arraycopy(CONFIG_LOCATIONS, 0, configLocations, 0, CONFIG_LOCATIONS.length); + configLocations[CONFIG_LOCATIONS.length] = appendTestConfigLocation; + } + TestWebScriptRepoServer.ctx = new ClassPathXmlApplicationContext(configLocations); + TestWebScriptRepoServer.appendedTestConfiguration = appendTestConfigLocation; + } + + // Get the bean + TestWebScriptServer testServer = (org.alfresco.repo.web.scripts.TestWebScriptRepoServer)TestWebScriptRepoServer.ctx.getBean("webscripts.test"); + return testServer; + } + + /** + * Interpret a single command using the BufferedReader passed in for any data needed. + * + * @param line The unparsed command + * @return The textual output of the command. + */ + @Override + protected String interpretCommand(final String line) + throws IOException + { + try + { + if (username.startsWith("TICKET_")) + { + try + { + retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + public Object execute() throws Exception + { + authenticationService.validate(username); + return null; + } + }); + return executeCommand(line); + } + finally + { + authenticationService.clearCurrentSecurityContext(); + } + } + } + catch(AuthenticationException e) + { + executeCommand("user " + getDefaultUserName()); + } + + // execute command in context of currently selected user + return AuthenticationUtil.runAs(new RunAsWork() + { + @SuppressWarnings("synthetic-access") + public String doWork() throws Exception + { + return executeCommand(line); + } + }, username); + } + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/test-context.xml b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/test-context.xml new file mode 100644 index 0000000000..6e094dcc19 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/test-context.xml @@ -0,0 +1,70 @@ + + + + + + + + + + org/alfresco/module/org_alfresco_module_rm/test/util/test-model.xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 30 3 * * ? + + + + + + + + + + + + + + + + + + + + + ${spaces.store} + ${spaces.archive.store} + + + + \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/test-model.xml b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/test-model.xml new file mode 100644 index 0000000000..5bf81115d2 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/test-model.xml @@ -0,0 +1,45 @@ + + + + + RM Test Model + Roy Wetherall + 1.0 + + + + + + + + + + + + + + + + + + + + + + cm:content + + + + + + + + + + + rma:recordMetaData + + + + + \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/BootstraptestDataRestApiTest.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/BootstraptestDataRestApiTest.java new file mode 100644 index 0000000000..58c486efdd --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/BootstraptestDataRestApiTest.java @@ -0,0 +1,78 @@ +/* + * 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.module.org_alfresco_module_rm.test.webscript; + +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.module.org_alfresco_module_rm.test.util.TestUtilities; +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.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.springframework.extensions.webscripts.TestWebScriptServer.GetRequest; + +/** + * This class tests the Rest API for disposition related operations + * + * @author Roy Wetherall + */ +public class BootstraptestDataRestApiTest extends BaseWebScriptTest implements RecordsManagementModel +{ + protected static StoreRef SPACES_STORE = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "SpacesStore"); + protected static final String URL = "/api/rma/bootstraptestdata"; + protected static final String SERVICE_URL_PREFIX = "/alfresco/service"; + protected static final String APPLICATION_JSON = "application/json"; + + protected NodeService nodeService; + protected RetryingTransactionHelper transactionHelper; + + @Override + protected void setUp() throws Exception + { + super.setUp(); + nodeService = (NodeService) getServer().getApplicationContext().getBean("NodeService"); + transactionHelper = (RetryingTransactionHelper)getServer().getApplicationContext().getBean("retryingTransactionHelper"); + } + + public void testBoostrapTestData() throws Exception + { + final NodeRef filePlan = transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + return TestUtilities.loadFilePlanData(getServer().getApplicationContext(), false, true); + } + }); + + sendRequest(new GetRequest(URL), 200); + + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + nodeService.deleteNode(filePlan); + return null; + } + }); + } + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/DispositionRestApiTest.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/DispositionRestApiTest.java new file mode 100644 index 0000000000..6795639b56 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/DispositionRestApiTest.java @@ -0,0 +1,633 @@ +/* + * 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.module.org_alfresco_module_rm.test.webscript; + +import java.io.Serializable; +import java.text.MessageFormat; +import java.util.Date; +import java.util.Map; + +import javax.transaction.UserTransaction; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService; +import org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEventService; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.module.org_alfresco_module_rm.test.util.TestUtilities; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.repo.web.scripts.BaseWebScriptTest; +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; +import org.alfresco.service.cmr.repository.Period; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.cmr.view.ImporterService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.json.JSONArray; +import org.json.JSONObject; +import org.json.JSONTokener; +import org.springframework.extensions.webscripts.TestWebScriptServer.DeleteRequest; +import org.springframework.extensions.webscripts.TestWebScriptServer.GetRequest; +import org.springframework.extensions.webscripts.TestWebScriptServer.PostRequest; +import org.springframework.extensions.webscripts.TestWebScriptServer.PutRequest; +import org.springframework.extensions.webscripts.TestWebScriptServer.Response; + +/** + * This class tests the Rest API for disposition related operations + * + * @author Gavin Cornwell + */ +public class DispositionRestApiTest extends BaseWebScriptTest implements RecordsManagementModel +{ + protected static StoreRef SPACES_STORE = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "SpacesStore"); + protected static final String GET_SCHEDULE_URL_FORMAT = "/api/node/{0}/dispositionschedule"; + protected static final String GET_LIFECYCLE_URL_FORMAT = "/api/node/{0}/nextdispositionaction"; + protected static final String POST_ACTIONDEF_URL_FORMAT = "/api/node/{0}/dispositionschedule/dispositionactiondefinitions"; + protected static final String DELETE_ACTIONDEF_URL_FORMAT = "/api/node/{0}/dispositionschedule/dispositionactiondefinitions/{1}"; + protected static final String PUT_ACTIONDEF_URL_FORMAT = "/api/node/{0}/dispositionschedule/dispositionactiondefinitions/{1}"; + protected static final String GET_LIST_URL = "/api/rma/admin/listofvalues"; + protected static final String SERVICE_URL_PREFIX = "/alfresco/service"; + protected static final String APPLICATION_JSON = "application/json"; + + protected NodeService nodeService; + protected ContentService contentService; + protected SearchService searchService; + protected ImporterService importService; + protected PermissionService permissionService; + protected TransactionService transactionService; + protected RecordsManagementService rmService; + protected RecordsManagementActionService rmActionService; + protected RecordsManagementEventService rmEventService; + protected RetryingTransactionHelper retryingTransactionHelper; + + @Override + protected void setUp() throws Exception + { + super.setUp(); + this.nodeService = (NodeService) getServer().getApplicationContext().getBean("NodeService"); + this.contentService = (ContentService)getServer().getApplicationContext().getBean("ContentService"); + this.searchService = (SearchService)getServer().getApplicationContext().getBean("SearchService"); + this.importService = (ImporterService)getServer().getApplicationContext().getBean("ImporterService"); + this.permissionService = (PermissionService)getServer().getApplicationContext().getBean("PermissionService"); + this.transactionService = (TransactionService)getServer().getApplicationContext().getBean("TransactionService"); + this.rmService = (RecordsManagementService)getServer().getApplicationContext().getBean("RecordsManagementService"); + this.rmActionService = (RecordsManagementActionService)getServer().getApplicationContext().getBean("RecordsManagementActionService"); + this.rmEventService = (RecordsManagementEventService)getServer().getApplicationContext().getBean("RecordsManagementEventService"); + this.retryingTransactionHelper = (RetryingTransactionHelper)getServer().getApplicationContext().getBean("retryingTransactionHelper"); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + + retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Void execute() throws Throwable + { + TestUtilities.loadFilePlanData(getServer().getApplicationContext()); + return null; + } + }); + + // Bring the filePlan into the test database. + // + // This is quite a slow call, so if this class grew to have many test methods, + // there would be a real benefit in using something like @BeforeClass for the line below. + //TestUtilities.loadFilePlanData(getServer().getApplicationContext()); + } + + public void testGetDispositionSchedule() throws Exception + { + // Test 404 status for non existent node + int expectedStatus = 404; + String nonExistentNode = "workspace/SpacesStore/09ca1e02-1c87-4a53-97e7-xxxxxxxxxxxx"; + String nonExistentUrl = MessageFormat.format(GET_SCHEDULE_URL_FORMAT, nonExistentNode); + Response rsp = sendRequest(new GetRequest(nonExistentUrl), expectedStatus); + + // Test 404 status for node that doesn't have dispostion schedule i.e. a record series + NodeRef series = TestUtilities.getRecordSeries(searchService, "Reports"); + assertNotNull(series); + String seriesNodeUrl = series.toString().replace("://", "/"); + String wrongNodeUrl = MessageFormat.format(GET_SCHEDULE_URL_FORMAT, seriesNodeUrl); + rsp = sendRequest(new GetRequest(wrongNodeUrl), expectedStatus); + + // Test data structure returned from "AIS Audit Records" + expectedStatus = 200; + NodeRef recordCategory = TestUtilities.getRecordCategory(this.searchService, "Reports", "AIS Audit Records"); + assertNotNull(recordCategory); + String categoryNodeUrl = recordCategory.toString().replace("://", "/"); + String requestUrl = MessageFormat.format(GET_SCHEDULE_URL_FORMAT, categoryNodeUrl); + rsp = sendRequest(new GetRequest(requestUrl), expectedStatus); + System.out.println(" 888 GET response: " + rsp.getContentAsString()); + assertEquals("application/json;charset=UTF-8", rsp.getContentType()); + + // get response as JSON + JSONObject jsonParsedObject = new JSONObject(new JSONTokener(rsp.getContentAsString())); + assertNotNull(jsonParsedObject); + + // check JSON data + JSONObject dataObj = jsonParsedObject.getJSONObject("data"); + assertNotNull(dataObj); + JSONObject rootDataObject = (JSONObject)dataObj; + assertEquals(10, rootDataObject.length()); + + // check individual data items + String serviceUrl = SERVICE_URL_PREFIX + requestUrl; + String url = rootDataObject.getString("url"); + assertEquals(serviceUrl, url); + + String authority = rootDataObject.getString("authority"); + assertEquals("N1-218-00-4 item 023", authority); + + String instructions = rootDataObject.getString("instructions"); + assertEquals("Cut off monthly, hold 1 month, then destroy.", instructions); + + String actionsUrl = rootDataObject.getString("actionsUrl"); + assertEquals(serviceUrl + "/dispositionactiondefinitions", actionsUrl); + + boolean recordLevel = rootDataObject.getBoolean("recordLevelDisposition"); + assertFalse(recordLevel); + + assertFalse(rootDataObject.getBoolean("canStepsBeRemoved")); + + JSONArray actions = rootDataObject.getJSONArray("actions"); + assertNotNull(actions); + assertEquals(2, actions.length()); + JSONObject action1 = (JSONObject)actions.get(0); + assertEquals(7, action1.length()); + assertNotNull(action1.get("id")); + assertNotNull(action1.get("url")); + assertEquals(0, action1.getInt("index")); + assertEquals("cutoff", action1.getString("name")); + assertEquals("Cutoff", action1.getString("label")); + assertEquals("monthend|1", action1.getString("period")); + assertTrue(action1.getBoolean("eligibleOnFirstCompleteEvent")); + + JSONObject action2 = (JSONObject)actions.get(1); + assertEquals(8, action2.length()); + assertEquals("rma:cutOffDate", action2.get("periodProperty")); + + // make sure the disposition schedule node ref is present and valid + String scheduleNodeRefJSON = rootDataObject.getString("nodeRef"); + NodeRef scheduleNodeRef = new NodeRef(scheduleNodeRefJSON); + assertTrue(this.nodeService.exists(scheduleNodeRef)); + + // Test data structure returned from "Personnel Security Program Records" + recordCategory = TestUtilities.getRecordCategory(this.searchService, "Civilian Files", "Employee Performance File System Records"); + assertNotNull(recordCategory); + categoryNodeUrl = recordCategory.toString().replace("://", "/"); + requestUrl = MessageFormat.format(GET_SCHEDULE_URL_FORMAT, categoryNodeUrl); + rsp = sendRequest(new GetRequest(requestUrl), expectedStatus); + //System.out.println("GET response: " + rsp.getContentAsString()); + assertEquals("application/json;charset=UTF-8", rsp.getContentType()); + + // get response as JSON + jsonParsedObject = new JSONObject(new JSONTokener(rsp.getContentAsString())); + assertNotNull(jsonParsedObject); + + // check JSON data + dataObj = jsonParsedObject.getJSONObject("data"); + assertNotNull(dataObj); + rootDataObject = (JSONObject)dataObj; + assertEquals(10, rootDataObject.length()); + + // check individual data items + serviceUrl = SERVICE_URL_PREFIX + requestUrl; + url = rootDataObject.getString("url"); + assertEquals(serviceUrl, url); + + authority = rootDataObject.getString("authority"); + assertEquals("GRS 1 item 23b(1)", authority); + + instructions = rootDataObject.getString("instructions"); + assertEquals("Cutoff when superseded. Destroy immediately after cutoff", instructions); + + recordLevel = rootDataObject.getBoolean("recordLevelDisposition"); + assertTrue(recordLevel); + + assertTrue(rootDataObject.getBoolean("canStepsBeRemoved")); + + actions = rootDataObject.getJSONArray("actions"); + assertNotNull(actions); + assertEquals(2, actions.length()); + action1 = (JSONObject)actions.get(0); + assertEquals(8, action1.length()); + assertNotNull(action1.get("id")); + assertNotNull(action1.get("url")); + assertEquals(0, action1.getInt("index")); + assertEquals("cutoff", action1.getString("name")); + assertEquals("Cutoff", action1.getString("label")); + assertTrue(action1.getBoolean("eligibleOnFirstCompleteEvent")); + JSONArray events = action1.getJSONArray("events"); + assertNotNull(events); + assertEquals(1, events.length()); + assertEquals("superseded", events.get(0)); + + // Test the retrieval of an empty disposition schedule + NodeRef recordSeries = TestUtilities.getRecordSeries(this.searchService, "Civilian Files"); + assertNotNull(recordSeries); + + // create a new recordCategory node in the recordSeries and then get + // the disposition schedule + NodeRef newRecordCategory = this.nodeService.createNode(recordSeries, ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, QName.createValidLocalName("recordCategory")), + TYPE_RECORD_CATEGORY).getChildRef(); + + categoryNodeUrl = newRecordCategory.toString().replace("://", "/"); + requestUrl = MessageFormat.format(GET_SCHEDULE_URL_FORMAT, categoryNodeUrl); + //System.out.println("GET response: " + rsp.getContentAsString()); + rsp = sendRequest(new GetRequest(requestUrl), expectedStatus); + + // get response as JSON + jsonParsedObject = new JSONObject(new JSONTokener(rsp.getContentAsString())); + System.out.println(rsp.getContentAsString()); + assertNotNull(jsonParsedObject); + + // check JSON data + dataObj = jsonParsedObject.getJSONObject("data"); + assertNotNull(dataObj); + rootDataObject = (JSONObject)dataObj; + assertEquals(8, rootDataObject.length()); + actions = rootDataObject.getJSONArray("actions"); + assertNotNull(actions); + assertEquals(0, actions.length()); + } + + public void testPostDispositionAction() throws Exception + { + // create a recordCategory to get a disposition schedule + NodeRef recordSeries = TestUtilities.getRecordSeries(this.searchService, "Civilian Files"); + assertNotNull(recordSeries); + + // create a new recordCategory node in the recordSeries and then get + // the disposition schedule + NodeRef newRecordCategory = this.nodeService.createNode(recordSeries, ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, QName.createValidLocalName("recordCategory")), + TYPE_RECORD_CATEGORY).getChildRef(); + + String categoryNodeUrl = newRecordCategory.toString().replace("://", "/"); + String requestUrl = MessageFormat.format(POST_ACTIONDEF_URL_FORMAT, categoryNodeUrl); + + // Construct the JSON request. + String name = "destroy"; + String desc = "Destroy this record after 5 years"; + String period = "year|5"; + String periodProperty = "rma:cutOffDate"; + boolean eligibleOnFirstCompleteEvent = true; + + JSONObject jsonPostData = new JSONObject(); + jsonPostData.put("name", name); + jsonPostData.put("description", desc); + jsonPostData.put("period", period); + jsonPostData.put("location", "my location"); + jsonPostData.put("periodProperty", periodProperty); + jsonPostData.put("eligibleOnFirstCompleteEvent", eligibleOnFirstCompleteEvent); + JSONArray events = new JSONArray(); + events.put("superseded"); + events.put("no_longer_needed"); + jsonPostData.put("events", events); + + // Submit the JSON request. + String jsonPostString = jsonPostData.toString(); + Response rsp = sendRequest(new PostRequest(requestUrl, jsonPostString, APPLICATION_JSON), 200); + + // check the returned data is what was expected + JSONObject jsonResponse = new JSONObject(new JSONTokener(rsp.getContentAsString())); + JSONObject dataObj = jsonResponse.getJSONObject("data"); + JSONObject rootDataObject = (JSONObject)dataObj; + assertNotNull(rootDataObject.getString("id")); + assertNotNull(rootDataObject.getString("url")); + assertEquals(0, rootDataObject.getInt("index")); + assertEquals(name, rootDataObject.getString("name")); + assertEquals("Destroy", rootDataObject.getString("label")); + assertEquals(desc, rootDataObject.getString("description")); + assertEquals(period, rootDataObject.getString("period")); + assertEquals("my location", rootDataObject.getString("location")); + assertEquals(periodProperty, rootDataObject.getString("periodProperty")); + assertTrue(rootDataObject.getBoolean("eligibleOnFirstCompleteEvent")); + events = rootDataObject.getJSONArray("events"); + assertNotNull(events); + assertEquals(2, events.length()); + assertEquals("superseded", events.get(0)); + assertEquals("no_longer_needed", events.get(1)); + + // test the minimum amount of data required to create an action definition + jsonPostData = new JSONObject(); + jsonPostData.put("name", name); + jsonPostString = jsonPostData.toString(); + rsp = sendRequest(new PostRequest(requestUrl, jsonPostString, APPLICATION_JSON), 200); + + // check the returned data is what was expected + jsonResponse = new JSONObject(new JSONTokener(rsp.getContentAsString())); + dataObj = jsonResponse.getJSONObject("data"); + assertNotNull(rootDataObject.getString("id")); + assertNotNull(rootDataObject.getString("url")); + assertEquals(0, rootDataObject.getInt("index")); + assertEquals(name, dataObj.getString("name")); + assertEquals("none|0", dataObj.getString("period")); + assertFalse(dataObj.has("description")); + assertFalse(dataObj.has("periodProperty")); + assertFalse(dataObj.has("events")); + assertTrue(dataObj.getBoolean("eligibleOnFirstCompleteEvent")); + + // negative test to ensure not supplying mandatory data results in an error + jsonPostData = new JSONObject(); + jsonPostData.put("description", desc); + jsonPostData.put("period", period); + jsonPostString = jsonPostData.toString(); + sendRequest(new PostRequest(requestUrl, jsonPostString, APPLICATION_JSON), 400); + } + + public void testPutDispositionAction() throws Exception + { + // create a new recordCategory node in the recordSeries and then get + // the disposition schedule + NodeRef recordSeries = TestUtilities.getRecordSeries(this.searchService, "Civilian Files"); + assertNotNull(recordSeries); + NodeRef newRecordCategory = this.nodeService.createNode(recordSeries, ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, QName.createValidLocalName("recordCategory")), + TYPE_RECORD_CATEGORY).getChildRef(); + + // create an action definition to then update + String categoryNodeUrl = newRecordCategory.toString().replace("://", "/"); + String postRequestUrl = MessageFormat.format(POST_ACTIONDEF_URL_FORMAT, categoryNodeUrl); + JSONObject jsonPostData = new JSONObject(); + jsonPostData.put("name", "cutoff"); + String jsonPostString = jsonPostData.toString(); + sendRequest(new PostRequest(postRequestUrl, jsonPostString, APPLICATION_JSON), 200); + + // verify the action definition is present and retrieve it's id + String getRequestUrl = MessageFormat.format(GET_SCHEDULE_URL_FORMAT, categoryNodeUrl); + Response rsp = sendRequest(new GetRequest(getRequestUrl), 200); + JSONObject json = new JSONObject(new JSONTokener(rsp.getContentAsString())); + JSONObject actionDef = json.getJSONObject("data").getJSONArray("actions").getJSONObject(0); + String actionDefId = actionDef.getString("id"); + assertEquals("cutoff", actionDef.getString("name")); + assertEquals("Cutoff", actionDef.getString("label")); + assertEquals("none|0", actionDef.getString("period")); + assertFalse(actionDef.has("description")); + assertFalse(actionDef.has("events")); + + // define body for PUT request + String name = "destroy"; + String desc = "Destroy this record after 5 years"; + String period = "year|5"; + String location = "my location"; + String periodProperty = "rma:cutOffDate"; + boolean eligibleOnFirstCompleteEvent = false; + + jsonPostData = new JSONObject(); + jsonPostData.put("name", name); + jsonPostData.put("description", desc); + jsonPostData.put("period", period); + jsonPostData.put("location", location); + jsonPostData.put("periodProperty", periodProperty); + jsonPostData.put("eligibleOnFirstCompleteEvent", eligibleOnFirstCompleteEvent); + JSONArray events = new JSONArray(); + events.put("superseded"); + events.put("no_longer_needed"); + jsonPostData.put("events", events); + jsonPostString = jsonPostData.toString(); + + // try and update a non existent action definition to check for 404 + String putRequestUrl = MessageFormat.format(PUT_ACTIONDEF_URL_FORMAT, categoryNodeUrl, "xyz"); + rsp = sendRequest(new PutRequest(putRequestUrl, jsonPostString, APPLICATION_JSON), 404); + + // update the action definition + putRequestUrl = MessageFormat.format(PUT_ACTIONDEF_URL_FORMAT, categoryNodeUrl, actionDefId); + rsp = sendRequest(new PutRequest(putRequestUrl, jsonPostString, APPLICATION_JSON), 200); + + // check the update happened correctly + json = new JSONObject(new JSONTokener(rsp.getContentAsString())); + actionDef = json.getJSONObject("data"); + assertEquals(name, actionDef.getString("name")); + assertEquals("Destroy", actionDef.getString("label")); + assertEquals(desc, actionDef.getString("description")); + assertEquals(period, actionDef.getString("period")); + assertEquals(location, actionDef.getString("location")); + assertEquals(periodProperty, actionDef.getString("periodProperty")); + assertFalse(actionDef.getBoolean("eligibleOnFirstCompleteEvent")); + assertEquals(2, actionDef.getJSONArray("events").length()); + assertEquals("superseded", actionDef.getJSONArray("events").getString(0)); + assertEquals("no_longer_needed", actionDef.getJSONArray("events").getString(1)); + } + + public void testDeleteDispositionAction() throws Exception + { + // create a new recordCategory node in the recordSeries and then get + // the disposition schedule + NodeRef recordSeries = TestUtilities.getRecordSeries(this.searchService, "Civilian Files"); + assertNotNull(recordSeries); + NodeRef newRecordCategory = this.nodeService.createNode(recordSeries, ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, QName.createValidLocalName("recordCategory")), + TYPE_RECORD_CATEGORY).getChildRef(); + + // create an action definition to then delete + String categoryNodeUrl = newRecordCategory.toString().replace("://", "/"); + String postRequestUrl = MessageFormat.format(POST_ACTIONDEF_URL_FORMAT, categoryNodeUrl); + JSONObject jsonPostData = new JSONObject(); + jsonPostData.put("name", "cutoff"); + String jsonPostString = jsonPostData.toString(); + sendRequest(new PostRequest(postRequestUrl, jsonPostString, APPLICATION_JSON), 200); + + // verify the action definition is present and retrieve it's id + String getRequestUrl = MessageFormat.format(GET_SCHEDULE_URL_FORMAT, categoryNodeUrl); + Response rsp = sendRequest(new GetRequest(getRequestUrl), 200); + JSONObject json = new JSONObject(new JSONTokener(rsp.getContentAsString())); + String actionDefId = json.getJSONObject("data").getJSONArray("actions").getJSONObject(0).getString("id"); + + // try and delete a non existent action definition to check for 404 + String deleteRequestUrl = MessageFormat.format(DELETE_ACTIONDEF_URL_FORMAT, categoryNodeUrl, "xyz"); + rsp = sendRequest(new DeleteRequest(deleteRequestUrl), 404); + + // now delete the action defintion created above + deleteRequestUrl = MessageFormat.format(DELETE_ACTIONDEF_URL_FORMAT, categoryNodeUrl, actionDefId); + rsp = sendRequest(new DeleteRequest(deleteRequestUrl), 200); + + // verify it got deleted + getRequestUrl = MessageFormat.format(GET_SCHEDULE_URL_FORMAT, categoryNodeUrl); + rsp = sendRequest(new GetRequest(getRequestUrl), 200); + json = new JSONObject(new JSONTokener(rsp.getContentAsString())); + JSONArray actions = json.getJSONObject("data").getJSONArray("actions"); + assertEquals(0, actions.length()); + } + + public void testGetDispositionLifecycle() throws Exception + { + // create a new recordFolder in a recordCategory + NodeRef recordCategory = TestUtilities.getRecordCategory(this.searchService, "Military Files", + "Military Assignment Documents"); + assertNotNull(recordCategory); + + // Test 404 for disposition lifecycle request on incorrect node + String categoryUrl = recordCategory.toString().replace("://", "/"); + String requestUrl = MessageFormat.format(GET_LIFECYCLE_URL_FORMAT, categoryUrl); + Response rsp = sendRequest(new GetRequest(requestUrl), 404); + + UserTransaction txn = transactionService.getUserTransaction(false); + txn.begin(); + + NodeRef newRecordFolder = this.nodeService.createNode(recordCategory, ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, QName.createValidLocalName("recordFolder")), + TYPE_RECORD_FOLDER).getChildRef(); + + txn.commit(); + txn = transactionService.getUserTransaction(false); + txn.begin(); + + // Create the document + NodeRef recordOne = this.nodeService.createNode(newRecordFolder, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "record"), + ContentModel.TYPE_CONTENT).getChildRef(); + + // Set the content + ContentWriter writer = this.contentService.getWriter(recordOne, ContentModel.PROP_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.setEncoding("UTF-8"); + writer.putContent("There is some content in this record"); + + txn.commit(); // - triggers FileAction + + txn = transactionService.getUserTransaction(false); + txn.begin(); + declareRecord(recordOne); + txn.commit(); + + // there should now be a disposition lifecycle for the record + String recordUrl = recordOne.toString().replace("://", "/"); + requestUrl = MessageFormat.format(GET_LIFECYCLE_URL_FORMAT, recordUrl); + rsp = sendRequest(new GetRequest(requestUrl), 200); + //System.out.println("GET : " + rsp.getContentAsString()); + assertEquals("application/json;charset=UTF-8", rsp.getContentType()); + + // get response as JSON + JSONObject jsonParsedObject = new JSONObject(new JSONTokener(rsp.getContentAsString())); + assertNotNull(jsonParsedObject); + + // check mandatory stuff is present + JSONObject dataObj = jsonParsedObject.getJSONObject("data"); + assertEquals(SERVICE_URL_PREFIX + requestUrl, dataObj.getString("url")); + assertEquals("cutoff", dataObj.getString("name")); + assertEquals("Cutoff", dataObj.getString("label")); + assertFalse(dataObj.getBoolean("eventsEligible")); + assertTrue(dataObj.has("events")); + JSONArray events = dataObj.getJSONArray("events"); + assertEquals(1, events.length()); + JSONObject event1 = events.getJSONObject(0); + assertEquals("superseded", event1.get("name")); + assertEquals("Superseded", event1.get("label")); + assertFalse(event1.getBoolean("complete")); + assertTrue(event1.getBoolean("automatic")); + + // check stuff expected to be missing is missing + assertFalse(dataObj.has("asOf")); + assertFalse(dataObj.has("startedAt")); + assertFalse(dataObj.has("startedBy")); + assertFalse(dataObj.has("completedAt")); + assertFalse(dataObj.has("completedBy")); + assertFalse(event1.has("completedAt")); + assertFalse(event1.has("completedBy")); + } + + public void testGetListOfValues() throws Exception + { + // call the list service + Response rsp = sendRequest(new GetRequest(GET_LIST_URL), 200); + //System.out.println("GET : " + rsp.getContentAsString()); + assertEquals("application/json;charset=UTF-8", rsp.getContentType()); + //System.out.println(rsp.getContentAsString()); + + // get response as JSON + JSONObject jsonParsedObject = new JSONObject(new JSONTokener(rsp.getContentAsString())); + assertNotNull(jsonParsedObject); + JSONObject data = jsonParsedObject.getJSONObject("data"); + + // check dispostion actions + JSONObject actions = data.getJSONObject("dispositionActions"); + assertEquals(SERVICE_URL_PREFIX + GET_LIST_URL + "/dispositionactions", actions.getString("url")); + JSONArray items = actions.getJSONArray("items"); + assertEquals(this.rmActionService.getDispositionActions().size(), items.length()); + assertTrue(items.length() > 0); + JSONObject item = items.getJSONObject(0); + assertTrue(item.length() == 2); + assertTrue(item.has("label")); + assertTrue(item.has("value")); + + // check events + JSONObject events = data.getJSONObject("events"); + assertEquals(SERVICE_URL_PREFIX + GET_LIST_URL + "/events", events.getString("url")); + items = events.getJSONArray("items"); + assertEquals(this.rmEventService.getEvents().size(), items.length()); + assertTrue(items.length() > 0); + item = items.getJSONObject(0); + assertTrue(item.length() == 3); + assertTrue(item.has("label")); + assertTrue(item.has("value")); + assertTrue(item.has("automatic")); + + // check period types + JSONObject periodTypes = data.getJSONObject("periodTypes"); + assertEquals(SERVICE_URL_PREFIX + GET_LIST_URL + "/periodtypes", periodTypes.getString("url")); + items = periodTypes.getJSONArray("items"); + assertEquals(Period.getProviderNames().size()-1, items.length()); + assertTrue(items.length() > 0); + item = items.getJSONObject(0); + assertTrue(item.length() == 2); + assertTrue(item.has("label")); + assertTrue(item.has("value")); + + // check period properties + JSONObject periodProperties = data.getJSONObject("periodProperties"); + assertEquals(SERVICE_URL_PREFIX + GET_LIST_URL + "/periodproperties", periodProperties.getString("url")); + items = periodProperties.getJSONArray("items"); + assertEquals(4, items.length()); + assertTrue(items.length() > 0); + item = items.getJSONObject(0); + assertTrue(item.length() == 2); + assertTrue(item.has("label")); + assertTrue(item.has("value")); + } + + private void declareRecord(NodeRef recordOne) + { + // Declare record + Map propValues = this.nodeService.getProperties(recordOne); + propValues.put(RecordsManagementModel.PROP_PUBLICATION_DATE, new Date()); + // List smList = new ArrayList(2); + // smList.add("FOUO"); + // smList.add("NOFORN"); + // propValues.put(RecordsManagementModel.PROP_SUPPLEMENTAL_MARKING_LIST, (Serializable)smList); + propValues.put(RecordsManagementModel.PROP_MEDIA_TYPE, "mediaTypeValue"); + propValues.put(RecordsManagementModel.PROP_FORMAT, "formatValue"); + propValues.put(RecordsManagementModel.PROP_DATE_RECEIVED, new Date()); + propValues.put(RecordsManagementModel.PROP_ORIGINATOR, "origValue"); + propValues.put(RecordsManagementModel.PROP_ORIGINATING_ORGANIZATION, "origOrgValue"); + propValues.put(ContentModel.PROP_TITLE, "titleValue"); + this.nodeService.setProperties(recordOne, propValues); + this.rmActionService.executeRecordsManagementAction(recordOne, "declareRecord"); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/EmailMapScriptTest.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/EmailMapScriptTest.java new file mode 100644 index 0000000000..90c0a574f8 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/EmailMapScriptTest.java @@ -0,0 +1,150 @@ +/* + * 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.module.org_alfresco_module_rm.test.webscript; + +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.web.scripts.BaseWebScriptTest; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.json.JSONArray; +import org.json.JSONObject; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.TestWebScriptServer.GetRequest; +import org.springframework.extensions.webscripts.TestWebScriptServer.PostRequest; +import org.springframework.extensions.webscripts.TestWebScriptServer.Response; + +public class EmailMapScriptTest extends BaseWebScriptTest +{ + + public final static String URL_RM_EMAILMAP = "/api/rma/admin/emailmap"; + + AuthenticationService authenticationService; + + @Override + protected void setUp() throws Exception + { + this.authenticationService = (AuthenticationService)getServer().getApplicationContext().getBean("AuthenticationService"); +// setCurrentUser(AuthenticationUtil.getAdminUserName()); + super.setUp(); + + // Set the current security context as admin + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + + } + + @Override + protected void tearDown() throws Exception + { + super.tearDown(); + + } + + public void testGetEmailMap() throws Exception + { + { + Response response = sendRequest(new GetRequest(URL_RM_EMAILMAP), Status.STATUS_OK); + + @SuppressWarnings("unused") + JSONObject top = new JSONObject(response.getContentAsString()); + System.out.println(response.getContentAsString()); + //JSONArray data = top.getJSONArray("data"); + } + } + + public void testUpdateEmailMap() throws Exception + { + /** + * Update the list by adding two values + */ + { + JSONObject obj = new JSONObject(); + + JSONArray add = new JSONArray(); + JSONObject val = new JSONObject(); + val.put("from", "whatever"); + val.put("to", "rmc:Wibble"); + add.put(val); + JSONObject val2 = new JSONObject(); + val2.put("from", "whatever"); + val2.put("to", "rmc:wobble"); + add.put(val2); + + obj.put("add", add); + + System.out.println(obj.toString()); + + /** + * Now do a post to add a couple of values + */ + Response response = sendRequest(new PostRequest(URL_RM_EMAILMAP, obj.toString(), "application/json"), Status.STATUS_OK); + System.out.println(response.getContentAsString()); + // Check the response + + + JSONArray delete = new JSONArray(); + delete.put(val2); + + } + + /** + * Update the list by deleting a value + * + * "whatever" has two mappings, delete one of them + */ + { + JSONObject obj = new JSONObject(); + JSONObject val2 = new JSONObject(); + JSONArray delete = new JSONArray(); + val2.put("from", "whatever"); + val2.put("to", "rmc:wobble"); + delete.put(val2); + obj.put("delete", delete); + + /** + * Now do a post to delete a couple of values + */ + Response response = sendRequest(new PostRequest(URL_RM_EMAILMAP, obj.toString(), "application/json"), Status.STATUS_OK); + System.out.println(response.getContentAsString()); + + JSONObject top = new JSONObject(response.getContentAsString()); + JSONObject data = top.getJSONObject("data"); + JSONArray mappings = data.getJSONArray("mappings"); + + boolean wibbleFound = false; + for(int i = 0; i < mappings.length(); i++) + { + JSONObject mapping = mappings.getJSONObject(i); + + + if(mapping.get("from").equals("whatever")) + { + if(mapping.get("to").equals("rmc:Wibble")) + { + wibbleFound = true; + } + else + { + fail("custom mapping for field not deleted"); + } + } + } + assertTrue(wibbleFound); + } + } +} + diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/EventRestApiTest.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/EventRestApiTest.java new file mode 100644 index 0000000000..c1b1aa5a66 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/EventRestApiTest.java @@ -0,0 +1,257 @@ +/* + * 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.module.org_alfresco_module_rm.test.webscript; + +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEventService; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.web.scripts.BaseWebScriptTest; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.util.GUID; +import org.springframework.extensions.webscripts.TestWebScriptServer.DeleteRequest; +import org.springframework.extensions.webscripts.TestWebScriptServer.GetRequest; +import org.springframework.extensions.webscripts.TestWebScriptServer.PostRequest; +import org.springframework.extensions.webscripts.TestWebScriptServer.PutRequest; +import org.springframework.extensions.webscripts.TestWebScriptServer.Response; +import org.json.JSONObject; + +/** + * RM event REST API test + * + * @author Roy Wetherall + */ +public class EventRestApiTest extends BaseWebScriptTest implements RecordsManagementModel +{ + protected static StoreRef SPACES_STORE = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "SpacesStore"); + protected static final String GET_EVENTS_URL = "/api/rma/admin/rmevents"; + protected static final String GET_EVENTTYPES_URL = "/api/rma/admin/rmeventtypes"; + protected static final String SERVICE_URL_PREFIX = "/alfresco/service"; + protected static final String APPLICATION_JSON = "application/json"; + protected static final String DISPLAY_LABEL = "display label"; + protected static final String EVENT_TYPE = "rmEventType.simple"; + protected static final String KEY_EVENT_NAME = "eventName"; + protected static final String KEY_EVENT_TYPE = "eventType"; + protected static final String KEY_EVENT_DISPLAY_LABEL = "eventDisplayLabel"; + + protected NodeService nodeService; + protected RecordsManagementService rmService; + protected RecordsManagementEventService rmEventService; + + @Override + protected void setUp() throws Exception + { + super.setUp(); + this.nodeService = (NodeService) getServer().getApplicationContext().getBean("NodeService"); + this.rmService = (RecordsManagementService)getServer().getApplicationContext().getBean("RecordsManagementService"); + this.rmEventService = (RecordsManagementEventService)getServer().getApplicationContext().getBean("RecordsManagementEventService"); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + } + + public void testGetEventTypes() throws Exception + { + Response rsp = sendRequest(new GetRequest(GET_EVENTTYPES_URL),200); + String rspContent = rsp.getContentAsString(); + + JSONObject obj = new JSONObject(rspContent); + JSONObject types = obj.getJSONObject("data"); + assertNotNull(types); + + JSONObject type = types.getJSONObject("rmEventType.simple"); + assertNotNull(type); + assertEquals("rmEventType.simple", type.getString("eventTypeName")); + assertNotNull(type.getString("eventTypeDisplayLabel")); + + System.out.println(rspContent); + } + + public void testGetEvents() throws Exception + { + String event1 = GUID.generate(); + String event2 = GUID.generate(); + + // Create a couple or events by hand + rmEventService.addEvent(EVENT_TYPE, event1, DISPLAY_LABEL); + rmEventService.addEvent(EVENT_TYPE, event2, DISPLAY_LABEL); + + try + { + // Get the events + Response rsp = sendRequest(new GetRequest(GET_EVENTS_URL),200); + String rspContent = rsp.getContentAsString(); + + JSONObject obj = new JSONObject(rspContent); + JSONObject roles = obj.getJSONObject("data"); + assertNotNull(roles); + + JSONObject eventObj = roles.getJSONObject(event1); + assertNotNull(eventObj); + assertEquals(event1, eventObj.get(KEY_EVENT_NAME)); + assertEquals(DISPLAY_LABEL, eventObj.get(KEY_EVENT_DISPLAY_LABEL)); + assertEquals(EVENT_TYPE, eventObj.get(KEY_EVENT_TYPE)); + + eventObj = roles.getJSONObject(event2); + assertNotNull(eventObj); + assertEquals(event2, eventObj.get(KEY_EVENT_NAME)); + assertEquals(DISPLAY_LABEL, eventObj.get(KEY_EVENT_DISPLAY_LABEL)); + assertEquals(EVENT_TYPE, eventObj.get(KEY_EVENT_TYPE)); + } + finally + { + // Clean up + rmEventService.removeEvent(event1); + rmEventService.removeEvent(event2); + } + + } + + public void testPostEvents() throws Exception + { + String eventName= GUID.generate(); + + JSONObject obj = new JSONObject(); + obj.put(KEY_EVENT_NAME, eventName); + obj.put(KEY_EVENT_DISPLAY_LABEL, DISPLAY_LABEL); + obj.put(KEY_EVENT_TYPE, EVENT_TYPE); + + Response rsp = sendRequest(new PostRequest(GET_EVENTS_URL, obj.toString(), APPLICATION_JSON),200); + try + { + String rspContent = rsp.getContentAsString(); + + JSONObject resultObj = new JSONObject(rspContent); + JSONObject eventObj = resultObj.getJSONObject("data"); + assertNotNull(eventObj); + + assertEquals(eventName, eventObj.get(KEY_EVENT_NAME)); + assertEquals(DISPLAY_LABEL, eventObj.get(KEY_EVENT_DISPLAY_LABEL)); + assertEquals(EVENT_TYPE, eventObj.get(KEY_EVENT_TYPE)); + + } + finally + { + rmEventService.removeEvent(eventName); + } + + // Test with no event name set + obj = new JSONObject(); + obj.put(KEY_EVENT_DISPLAY_LABEL, DISPLAY_LABEL); + obj.put(KEY_EVENT_TYPE, EVENT_TYPE); + rsp = sendRequest(new PostRequest(GET_EVENTS_URL, obj.toString(), APPLICATION_JSON),200); + try + { + String rspContent = rsp.getContentAsString(); + + JSONObject resultObj = new JSONObject(rspContent); + JSONObject eventObj = resultObj.getJSONObject("data"); + assertNotNull(eventObj); + + assertNotNull(eventObj.get(KEY_EVENT_NAME)); + assertEquals(DISPLAY_LABEL, eventObj.get(KEY_EVENT_DISPLAY_LABEL)); + assertEquals(EVENT_TYPE, eventObj.get(KEY_EVENT_TYPE)); + + eventName = eventObj.getString(KEY_EVENT_NAME); + } + finally + { + rmEventService.removeEvent(eventName); + } + } + + public void testPutRole() throws Exception + { + String eventName = GUID.generate(); + rmEventService.addEvent(EVENT_TYPE, eventName, DISPLAY_LABEL); + + try + { + JSONObject obj = new JSONObject(); + obj.put(KEY_EVENT_NAME, eventName); + obj.put(KEY_EVENT_DISPLAY_LABEL, "changed"); + obj.put(KEY_EVENT_TYPE, EVENT_TYPE); + + // Get the roles + Response rsp = sendRequest(new PutRequest(GET_EVENTS_URL + "/" + eventName, obj.toString(), APPLICATION_JSON),200); + String rspContent = rsp.getContentAsString(); + + JSONObject result = new JSONObject(rspContent); + JSONObject eventObj = result.getJSONObject("data"); + assertNotNull(eventObj); + + assertEquals(eventName, eventObj.get(KEY_EVENT_NAME)); + assertEquals("changed", eventObj.get(KEY_EVENT_DISPLAY_LABEL)); + assertEquals(EVENT_TYPE, eventObj.get(KEY_EVENT_TYPE)); + + // Bad requests + sendRequest(new PutRequest(GET_EVENTS_URL + "/cheese", obj.toString(), APPLICATION_JSON), 404); + } + finally + { + // Clean up + rmEventService.removeEvent(eventName); + } + + } + + public void testGetRole() throws Exception + { + String eventName = GUID.generate(); + rmEventService.addEvent(EVENT_TYPE, eventName, DISPLAY_LABEL); + + try + { + // Get the roles + Response rsp = sendRequest(new GetRequest(GET_EVENTS_URL + "/" + eventName),200); + String rspContent = rsp.getContentAsString(); + + JSONObject obj = new JSONObject(rspContent); + JSONObject eventObj = obj.getJSONObject("data"); + assertNotNull(eventObj); + + assertEquals(eventName, eventObj.get(KEY_EVENT_NAME)); + assertEquals(DISPLAY_LABEL, eventObj.get(KEY_EVENT_DISPLAY_LABEL)); + assertEquals(EVENT_TYPE, eventObj.get(KEY_EVENT_TYPE)); + + // Bad requests + sendRequest(new GetRequest(GET_EVENTS_URL + "/cheese"), 404); + } + finally + { + // Clean up + rmEventService.removeEvent(eventName); + } + + } + + public void testDeleteRole() throws Exception + { + String eventName = GUID.generate(); + assertFalse(rmEventService.existsEvent(eventName)); + rmEventService.addEvent(EVENT_TYPE, eventName, DISPLAY_LABEL); + assertTrue(rmEventService.existsEvent(eventName)); + sendRequest(new DeleteRequest(GET_EVENTS_URL + "/" + eventName),200); + assertFalse(rmEventService.existsEvent(eventName)); + + // Bad request + sendRequest(new DeleteRequest(GET_EVENTS_URL + "/cheese"), 404); + } + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RMCaveatConfigScriptTest.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RMCaveatConfigScriptTest.java new file mode 100644 index 0000000000..4caab0b1ca --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RMCaveatConfigScriptTest.java @@ -0,0 +1,971 @@ +/* + * 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.module.org_alfresco_module_rm.test.webscript; + +import java.util.ArrayList; +import java.util.List; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.caveat.RMCaveatConfigService; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.web.scripts.BaseWebScriptTest; +import org.alfresco.service.cmr.security.MutableAuthenticationService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.util.PropertyMap; +import org.json.JSONArray; +import org.json.JSONObject; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.TestWebScriptServer.DeleteRequest; +import org.springframework.extensions.webscripts.TestWebScriptServer.GetRequest; +import org.springframework.extensions.webscripts.TestWebScriptServer.PostRequest; +import org.springframework.extensions.webscripts.TestWebScriptServer.PutRequest; +import org.springframework.extensions.webscripts.TestWebScriptServer.Response; + +/** + * + * + * @author Mark Rogers + */ +public class RMCaveatConfigScriptTest extends BaseWebScriptTest +{ + private MutableAuthenticationService authenticationService; + private RMCaveatConfigService caveatConfigService; + private PersonService personService; + + private static final String USER_ONE = "RMCaveatConfigTestOne"; + private static final String USER_TWO = "RMCaveatConfigTestTwo"; + + protected final static String RM_LIST = "rmc:smListTest"; + protected final static String RM_LIST_URI_ELEM = "rmc_smListTest"; + + private static final String URL_RM_CONSTRAINTS = "/api/rma/admin/rmconstraints"; + + @Override + protected void setUp() throws Exception + { + super.setUp(); + + this.caveatConfigService = (RMCaveatConfigService)getServer().getApplicationContext().getBean("CaveatConfigService"); + this.authenticationService = (MutableAuthenticationService)getServer().getApplicationContext().getBean("AuthenticationService"); + this.personService = (PersonService)getServer().getApplicationContext().getBean("PersonService"); + + // Set the current security context as admin + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + } + + private void createUser(String userName) + { + if (this.authenticationService.authenticationExists(userName) == false) + { + this.authenticationService.createAuthentication(userName, "PWD".toCharArray()); + + PropertyMap ppOne = new PropertyMap(4); + ppOne.put(ContentModel.PROP_USERNAME, userName); + ppOne.put(ContentModel.PROP_AUTHORITY_DISPLAY_NAME, "title" + userName); + ppOne.put(ContentModel.PROP_FIRSTNAME, "firstName"); + ppOne.put(ContentModel.PROP_LASTNAME, "lastName"); + ppOne.put(ContentModel.PROP_EMAIL, "email@email.com"); + ppOne.put(ContentModel.PROP_JOBTITLE, "jobTitle"); + + this.personService.createPerson(ppOne); + } + } + + @Override + protected void tearDown() throws Exception + { + super.tearDown(); + //this.authenticationComponent.setCurrentUser(AuthenticationUtil.getAdminUserName()); + + } + + + public void testGetRMConstraints() throws Exception + { + { + Response response = sendRequest(new GetRequest(URL_RM_CONSTRAINTS), Status.STATUS_OK); + + JSONObject top = new JSONObject(response.getContentAsString()); + System.out.println(response.getContentAsString()); + JSONArray data = top.getJSONArray("data"); + } + + /** + * Add a list, then get it back via the list rest script + */ + caveatConfigService.addRMConstraint(RM_LIST, "my title", new String[0]); + + { + Response response = sendRequest(new GetRequest(URL_RM_CONSTRAINTS), Status.STATUS_OK); + JSONObject top = new JSONObject(response.getContentAsString()); + System.out.println(response.getContentAsString()); + JSONArray data = top.getJSONArray("data"); + + boolean found = false; + assertTrue("no data returned", data.length() > 0); + for(int i = 0; i < data.length(); i++) + { + JSONObject obj = data.getJSONObject(i); + String name = (String)obj.getString("constraintName"); + assertNotNull("constraintName is null", name); + String url = (String)obj.getString("url"); + assertNotNull("detail url is null", name); + if(name.equalsIgnoreCase(RM_LIST)) + { + found = true; + } + + /** + * vallidate the detail URL returned + */ + sendRequest(new GetRequest(url), Status.STATUS_OK); + } + } + } + + /** + * + * @throws Exception + */ + public void testGetRMConstraint() throws Exception + { + /** + * Delete the list to remove any junk then recreate it. + */ + if (caveatConfigService.getRMConstraint(RM_LIST) != null) + { + caveatConfigService.deleteRMConstraint(RM_LIST); + } + caveatConfigService.addRMConstraint(RM_LIST, "my title", new String[0]); + + + createUser("fbloggs"); + createUser("jrogers"); + createUser("jdoe"); + + + List values = new ArrayList(); + values.add("NOFORN"); + values.add("FGI"); + caveatConfigService.updateRMConstraintListAuthority(RM_LIST, "fbloggs", values); + caveatConfigService.updateRMConstraintListAuthority(RM_LIST, "jrogers", values); + caveatConfigService.updateRMConstraintListAuthority(RM_LIST, "jdoe", values); + + /** + * Positive test Get the constraint + */ + { + String url = URL_RM_CONSTRAINTS + "/" + RM_LIST_URI_ELEM; + Response response = sendRequest(new GetRequest(url), Status.STATUS_OK); + JSONObject top = new JSONObject(response.getContentAsString()); + + JSONObject data = top.getJSONObject("data"); + System.out.println(response.getContentAsString()); + + String constraintName = data.getString("constraintName"); + assertNotNull("constraintName is null", constraintName); + JSONArray allowedValues = data.getJSONArray("allowedValues"); + +// assertTrue("values not correct", compare(array, allowedValues)); + +// JSONArray constraintDetails = data.getJSONArray("constraintDetails"); +// +// assertTrue("details array does not contain 3 elements", constraintDetails.length() == 3); +// for(int i =0; i < constraintDetails.length(); i++) +// { +// JSONObject detail = constraintDetails.getJSONObject(i); +// } + } + + /** + * + * @throws Exception + */ + + /** + * Negative test - Attempt to get a constraint that does exist + */ + { + String url = URL_RM_CONSTRAINTS + "/" + "rmc_wibble"; + sendRequest(new GetRequest(url), Status.STATUS_NOT_FOUND); + } + + personService.deletePerson("fbloggs"); + personService.deletePerson("jrogers"); + personService.deletePerson("jdoe"); + + + + + } + + /** + * Create an RM Constraint + * @throws Exception + */ + public void testUpdateRMConstraint() throws Exception + { + + String constraintName = null; + /* + * Create a new list + */ + { + String title = "test Update RM Constraint title"; + JSONArray array = new JSONArray(); + array.put("LEMON"); + array.put("BANANA"); + array.put("PEACH"); + + JSONObject obj = new JSONObject(); + obj.put("allowedValues", array); + obj.put("constraintTitle", title); + /** + * Now do a post to create a new list + */ + Response response = sendRequest(new PostRequest(URL_RM_CONSTRAINTS, obj.toString(), "application/json"), Status.STATUS_OK); + JSONObject top = new JSONObject(response.getContentAsString()); + + JSONObject data = top.getJSONObject("data"); + constraintName = data.getString("constraintName"); + JSONArray allowedValues = data.getJSONArray("allowedValues"); + assertTrue("values not correct", compare(array, allowedValues)); + + } + + /** + * Now update both values and title - remove BANANA, PEACH, Add APPLE. + */ + + { + String newTitle = "this is the new title"; + JSONArray array = new JSONArray(); + array.put("LEMON"); + array.put("APPLE"); + + JSONObject obj = new JSONObject(); + obj.put("allowedValues", array); + obj.put("constraintName", constraintName); + obj.put("constraintTitle", newTitle); + + System.out.println(obj.toString()); + + /** + * Now do a post to update list + */ + Response response = sendRequest(new PutRequest(URL_RM_CONSTRAINTS + "/" + constraintName, obj.toString(), "application/json"), Status.STATUS_OK); + // Check the response + JSONObject top = new JSONObject(response.getContentAsString()); + JSONObject data = top.getJSONObject("data"); + + System.out.println(response.getContentAsString()); + + String url = data.getString("url"); + String constraintName2 = data.getString("constraintName"); + String constraintTitle = data.getString("constraintTitle"); + JSONArray allowedValues = data.getJSONArray("allowedValues"); + + assertTrue(allowedValues.length() == 2); + assertTrue("values not correct", compare(array, allowedValues)); + assertNotNull(url); + assertEquals(constraintName2, constraintName); + assertNotNull(constraintTitle); + assertEquals("title not as expected", constraintTitle, newTitle); + + // Check that data has been persisted. + Response resp2 = sendRequest(new GetRequest(url), Status.STATUS_OK); + JSONObject top2 = new JSONObject(resp2.getContentAsString()); + System.out.println("Problem here"); + System.out.println(resp2.getContentAsString()); + JSONObject data2 = top2.getJSONObject("data"); + String constraintTitle2 = data2.getString("constraintTitle"); + JSONArray allowedValues2 = data2.getJSONArray("allowedValues"); + assertTrue("values not correct", compare(array, allowedValues2)); + assertTrue("allowedValues is not 2", allowedValues2.length() == 2); + assertEquals(constraintName2, constraintName); + assertNotNull(constraintTitle2); + assertEquals("title not as expected", constraintTitle2, newTitle); + + } + + /** + * Now put without allowed values + */ + { + String newTitle = "update with no values"; + + JSONObject obj = new JSONObject(); + + obj.put("constraintName", RM_LIST); + obj.put("constraintTitle", newTitle); + + /** + * Now do a put to update a new list + */ + + Response response = sendRequest(new PutRequest(URL_RM_CONSTRAINTS + "/" + constraintName, obj.toString(), "application/json"), Status.STATUS_OK); + // Check the response + JSONObject top = new JSONObject(response.getContentAsString()); + + JSONObject data = top.getJSONObject("data"); + System.out.println(response.getContentAsString()); + + String url = data.getString("url"); + String constraintName2 = data.getString("constraintName"); + String constraintTitle = data.getString("constraintTitle"); + JSONArray allowedValues = data.getJSONArray("allowedValues"); + + assertTrue(allowedValues.length() == 2); + + assertNotNull(url); + assertEquals(constraintName2, constraintName); + assertNotNull(constraintTitle); + assertEquals("title not as expected", constraintTitle, newTitle); + } + + /** + * Now post without constraint Title + */ + { + JSONArray array = new JSONArray(); + array.put("LEMON"); + array.put("APPLE"); + + JSONObject obj = new JSONObject(); + obj.put("allowedValues", array); + + System.out.println(obj.toString()); + + /** + * Now do a Put to update the list - title should remain + */ + + Response response = sendRequest(new PutRequest(URL_RM_CONSTRAINTS + "/" + constraintName, obj.toString(), "application/json"), Status.STATUS_OK); + // Check the response + JSONObject top = new JSONObject(response.getContentAsString()); + + JSONObject data = top.getJSONObject("data"); + System.out.println(response.getContentAsString()); + } + + /** + * Add a new value (PEAR) to the list + */ + { + JSONArray array = new JSONArray(); + array.put("PEAR"); + array.put("LEMON"); + array.put("APPLE"); + + JSONObject obj = new JSONObject(); + obj.put("allowedValues", array); + + System.out.println(obj.toString()); + + Response response = sendRequest(new PutRequest(URL_RM_CONSTRAINTS + "/" + constraintName, obj.toString(), "application/json"), Status.STATUS_OK); + // Check the response + JSONObject top = new JSONObject(response.getContentAsString()); + + JSONObject data = top.getJSONObject("data"); + System.out.println(response.getContentAsString()); + } + + /** + * Remove a value (PEAR) from the list + */ + { + JSONArray array = new JSONArray(); + array.put("APPLE"); + array.put("LEMON"); + + JSONObject obj = new JSONObject(); + obj.put("allowedValues", array); + + System.out.println(obj.toString()); + + Response response = sendRequest(new PutRequest(URL_RM_CONSTRAINTS + "/" + constraintName, obj.toString(), "application/json"), Status.STATUS_OK); + // Check the response + JSONObject top = new JSONObject(response.getContentAsString()); + + JSONObject data = top.getJSONObject("data"); + System.out.println(response.getContentAsString()); + } + + } + + + /** + * Create an RM Constraint + * @throws Exception + */ + public void testCreateRMConstraint() throws Exception + { + /** + * Delete the list to remove any junk then recreate it. + */ + //caveatConfigService.deleteRMConstraint(RM_LIST); + + /** + * create a new list + */ + { + JSONArray array = new JSONArray(); + array.put("NOFORN"); + array.put("FGI"); + + JSONObject obj = new JSONObject(); + obj.put("allowedValues", array); + obj.put("constraintName", RM_LIST); + obj.put("constraintTitle", "this is the title"); + + System.out.println(obj.toString()); + + /** + * Now do a post to create a new list + */ + + Response response = sendRequest(new PostRequest(URL_RM_CONSTRAINTS, obj.toString(), "application/json"), Status.STATUS_OK); + // Check the response + } + + /** + * Now go and get the constraint + */ + { + String url = URL_RM_CONSTRAINTS + "/" + RM_LIST_URI_ELEM; + Response response = sendRequest(new GetRequest(url), Status.STATUS_OK); + JSONObject top = new JSONObject(response.getContentAsString()); + + JSONObject data = top.getJSONObject("data"); + System.out.println(response.getContentAsString()); + + String constraintName = data.getString("constraintName"); + assertNotNull("constraintName is null", constraintName); + +// JSONArray constraintDetails = data.getJSONArray("constraintDetails"); +// +// assertTrue("details array does not contain 3 elements", constraintDetails.length() == 3); +// for(int i =0; i < constraintDetails.length(); i++) +// { +// JSONObject detail = constraintDetails.getJSONObject(i); +// } + } + + /** + * Now a constraint with a generated name + */ + { + String title = "Generated title list"; + JSONArray array = new JSONArray(); + array.put("Red"); + array.put("Blue"); + array.put("Green"); + + JSONObject obj = new JSONObject(); + obj.put("allowedValues", array); + obj.put("constraintTitle", title); + + System.out.println(obj.toString()); + + /** + * Now do a post to create a new list + */ + Response response = sendRequest(new PostRequest(URL_RM_CONSTRAINTS, obj.toString(), "application/json"), Status.STATUS_OK); + JSONObject top = new JSONObject(response.getContentAsString()); + + JSONObject data = top.getJSONObject("data"); + System.out.println(response.getContentAsString()); + + // Check the response + + String url = data.getString("url"); + String constraintName = data.getString("constraintName"); + String constraintTitle = data.getString("constraintTitle"); + JSONArray allowedValues = data.getJSONArray("allowedValues"); + + assertTrue(allowedValues.length() == 3); + assertNotNull(url); + assertNotNull(constraintName); + assertNotNull(constraintTitle); + assertEquals("title not as expected", constraintTitle, title); + sendRequest(new GetRequest(url), Status.STATUS_OK); + + + } + + + /** + * Now a constraint with an empty list of values. + */ + { + JSONArray array = new JSONArray(); + + JSONObject obj = new JSONObject(); + obj.put("allowedValues", array); + obj.put("constraintName", "rmc_whazoo"); + obj.put("constraintTitle", "this is the title"); + + System.out.println(obj.toString()); + + /** + * Now do a post to create a new list + */ + + Response response = sendRequest(new PostRequest(URL_RM_CONSTRAINTS, obj.toString(), "application/json"), Status.STATUS_OK); + JSONObject top = new JSONObject(response.getContentAsString()); + + JSONObject data = top.getJSONObject("data"); + System.out.println(response.getContentAsString()); + + // Check the response + } + + +// /** +// * Negative tests - duplicate list +// */ +// { +// JSONArray array = new JSONArray(); +// array.put("NOFORN"); +// array.put("FGI"); +// +// JSONObject obj = new JSONObject(); +// obj.put("allowedValues", array); +// obj.put("constraintName", RM_LIST); +// obj.put("constraintTitle", "this is the title"); +// +// System.out.println(obj.toString()); +// +// /** +// * Now do a post to create a new list +// */ +// Response response = sendRequest(new PostRequest(URL_RM_CONSTRAINTS, obj.toString(), "application/json"), Status.STATUS_CREATED); +// JSONObject top = new JSONObject(response.getContentAsString()); +// +// JSONObject data = top.getJSONObject("data"); +// System.out.println(response.getContentAsString()); +// +// // Check the response +// } + + + } + + + public void testGetRMConstraintValues() throws Exception + { + createUser("fbloggs"); + createUser("jrogers"); + createUser("jdoe"); + + /** + * Delete the list to remove any junk then recreate it. + */ + { + if (caveatConfigService.getRMConstraint(RM_LIST) != null) + { + caveatConfigService.deleteRMConstraint(RM_LIST); + } + caveatConfigService.addRMConstraint(RM_LIST, "my title", new String[0]); + + List values = new ArrayList(); + values.add("NOFORN"); + values.add("FGI"); + caveatConfigService.updateRMConstraintListAuthority(RM_LIST, "fbloggs", values); + caveatConfigService.updateRMConstraintListAuthority(RM_LIST, "jrogers", values); + caveatConfigService.updateRMConstraintListAuthority(RM_LIST, "jdoe", values); + } + + /** + * Positive test Get the constraint + */ + { + String url = URL_RM_CONSTRAINTS + "/" + RM_LIST_URI_ELEM + "/values"; + Response response = sendRequest(new GetRequest(url), Status.STATUS_OK); + JSONObject top = new JSONObject(response.getContentAsString()); + + JSONObject data = top.getJSONObject("data"); + System.out.println(response.getContentAsString()); + + String constraintName = data.getString("constraintName"); + assertNotNull("constraintName is null", constraintName); + String constraintTitle = data.getString("constraintTitle"); + assertNotNull("constraintTitle is null", constraintTitle); + + JSONArray values = data.getJSONArray("values"); + + assertTrue("details array does not contain 2 elements", values.length() == 2); + boolean fgiFound = false; + boolean nofornFound = false; + + for(int i =0; i < values.length(); i++) + { + JSONObject value = values.getJSONObject(i); + + if(value.getString("valueName").equalsIgnoreCase("FGI")) + { + fgiFound = true; + + } + + if(value.getString("valueName").equalsIgnoreCase("NOFORN")) + { + nofornFound = true; + } + + + } + assertTrue("fgi not found", fgiFound); + assertTrue("noforn not found", nofornFound); + } + + personService.deletePerson("fbloggs"); + personService.deletePerson("jrogers"); + personService.deletePerson("jdoe"); + } + + + + /** + * Update a value in a constraint + * @throws Exception + */ + public void testUpdateRMConstraintValue() throws Exception + { + if (caveatConfigService.getRMConstraint(RM_LIST) != null) + { + caveatConfigService.deleteRMConstraint(RM_LIST); + } + caveatConfigService.addRMConstraint(RM_LIST, "my title", new String[0]); + + /** + * Add some data to an empty list + */ + { + JSONArray values = new JSONArray(); + + JSONArray authorities = new JSONArray(); + authorities.put("fbloggs"); + authorities.put("jdoe"); + + JSONObject valueA = new JSONObject(); + valueA.put("value", "NOFORN"); + valueA.put("authorities", authorities); + + values.put(valueA); + + JSONObject valueB = new JSONObject(); + valueB.put("value", "FGI"); + valueB.put("authorities", authorities); + + values.put(valueB); + + JSONObject obj = new JSONObject(); + obj.put("values", values); + + + /** + * Do the first update - should get back + * NOFORN - fbloggs, jdoe + * FGI - fbloggs, jdoe + */ + Response response = sendRequest(new PostRequest(URL_RM_CONSTRAINTS + "/" + RM_LIST + "/values" , obj.toString(), "application/json"), Status.STATUS_OK); + JSONObject top = new JSONObject(response.getContentAsString()); + + JSONObject data = top.getJSONObject("data"); + System.out.println(response.getContentAsString()); + assertNotNull("data is null", data); + + JSONArray myValues = data.getJSONArray("values"); + assertTrue("two values not found", myValues.length() == 2); + for(int i = 0; i < myValues.length(); i++) + { + JSONObject myObj = myValues.getJSONObject(i); + } + } + + /** + * Add to a new value, NOCON, fbloggs, jrogers + */ + { + JSONArray values = new JSONArray(); + + JSONArray authorities = new JSONArray(); + authorities.put("fbloggs"); + authorities.put("jrogers"); + + JSONObject valueA = new JSONObject(); + valueA.put("value", "NOCON"); + valueA.put("authorities", authorities); + + values.put(valueA); + + + JSONObject obj = new JSONObject(); + obj.put("values", values); + + + /** + * Add a new value - should get back + * NOFORN - fbloggs, jdoe + * FGI - fbloggs, jdoe + * NOCON - fbloggs, jrogers + */ + System.out.println(obj.toString()); + Response response = sendRequest(new PostRequest(URL_RM_CONSTRAINTS + "/" + RM_LIST + "/values" , obj.toString(), "application/json"), Status.STATUS_OK); + JSONObject top = new JSONObject(response.getContentAsString()); + + JSONObject data = top.getJSONObject("data"); + System.out.println(response.getContentAsString()); + assertNotNull("data is null", data); + + JSONArray myValues = data.getJSONArray("values"); + assertTrue("three values not found", myValues.length() == 3); + for(int i = 0; i < myValues.length(); i++) + { + JSONObject myObj = myValues.getJSONObject(i); + } + } + + /** + * Add to an existing value (NOFORN, jrogers) + * should get back + * NOFORN - fbloggs, jdoe, jrogers + * FGI - fbloggs, jdoe + * NOCON - fbloggs, jrogers + */ + { + JSONArray values = new JSONArray(); + + JSONArray authorities = new JSONArray(); + authorities.put("fbloggs"); + authorities.put("jrogers"); + authorities.put("jdoe"); + + JSONObject valueA = new JSONObject(); + valueA.put("value", "NOFORN"); + valueA.put("authorities", authorities); + + values.put(valueA); + + + JSONObject obj = new JSONObject(); + obj.put("values", values); + + Response response = sendRequest(new PostRequest(URL_RM_CONSTRAINTS + "/" + RM_LIST + "/values" , obj.toString(), "application/json"), Status.STATUS_OK); + JSONObject top = new JSONObject(response.getContentAsString()); + + JSONObject data = top.getJSONObject("data"); + System.out.println(response.getContentAsString()); + assertNotNull("data is null", data); + + JSONArray myValues = data.getJSONArray("values"); + assertTrue("three values not found", myValues.length() == 3); + for(int i = 0; i < myValues.length(); i++) + { + JSONObject myObj = myValues.getJSONObject(i); + } + } + + + /** + * Remove from existing value (NOCON, fbloggs) + */ + { + JSONArray values = new JSONArray(); + + JSONArray authorities = new JSONArray(); + authorities.put("jrogers"); + + JSONObject valueA = new JSONObject(); + valueA.put("value", "NOCON"); + valueA.put("authorities", authorities); + + values.put(valueA); + + + JSONObject obj = new JSONObject(); + obj.put("values", values); + + + /** + * should get back + * NOFORN - fbloggs, jdoe + * FGI - fbloggs, jdoe + * NOCON - jrogers + */ + Response response = sendRequest(new PostRequest(URL_RM_CONSTRAINTS + "/" + RM_LIST + "/values" , obj.toString(), "application/json"), Status.STATUS_OK); + JSONObject top = new JSONObject(response.getContentAsString()); + + JSONObject data = top.getJSONObject("data"); + System.out.println(response.getContentAsString()); + assertNotNull("data is null", data); + + JSONArray myValues = data.getJSONArray("values"); + assertTrue("three values not found", myValues.length() == 3); + boolean foundNOCON = false; + boolean foundNOFORN = false; + boolean foundFGI = false; + + for(int i = 0; i < myValues.length(); i++) + { + JSONObject myObj = myValues.getJSONObject(i); + + if(myObj.getString("valueName").equalsIgnoreCase("NOCON")) + { + foundNOCON = true; + } + if(myObj.getString("valueName").equalsIgnoreCase("NOFORN")) + { + foundNOFORN = true; + } + if(myObj.getString("valueName").equalsIgnoreCase("FGI")) + { + foundFGI = true; + } + } + + assertTrue("not found NOCON", foundNOCON); + assertTrue("not found NOFORN", foundNOFORN); + assertTrue("not found FGI", foundFGI); + } + } + + + /** + * Delete the entire constraint + * + * @throws Exception + */ + public void testDeleteRMConstraint() throws Exception + { + /** + * Delete the list to remove any junk then recreate it. + */ + if (caveatConfigService.getRMConstraint(RM_LIST) != null) + { + caveatConfigService.deleteRMConstraint(RM_LIST); + } + caveatConfigService.addRMConstraint(RM_LIST, "my title", new String[0]); + + /** + * Now do a delete + */ + Response response = sendRequest(new DeleteRequest(URL_RM_CONSTRAINTS + "/" + RM_LIST), Status.STATUS_OK); + + /** + * Now delete the list that should have been deleted + */ + // TODO NEED TO THINK ABOUT THIS BEHAVIOUR + //{ + // sendRequest(new DeleteRequest(URL_RM_CONSTRAINTS + "/" + RM_LIST), Status.STATUS_NOT_FOUND); + //} + + /** + * Negative test - delete list that does not exist + */ + { + sendRequest(new DeleteRequest(URL_RM_CONSTRAINTS + "/" + "rmc_wibble"), Status.STATUS_NOT_FOUND); + } + } + + private boolean compare(JSONArray from, JSONArray to) throws Exception + { + List ret = new ArrayList(); + + if(from.length() != to.length()) + { + fail("arrays are different lengths" + from.length() +", " + to.length()); + return false; + } + + for(int i = 0 ; i < to.length(); i++) + { + ret.add(to.getString(i)); + } + + for(int i = 0 ; i < from.length(); i++) + { + String val = from.getString(i); + + if(ret.contains(val)) + { + + } + else + { + fail("Value not contained in list:" + val); + return false; + } + } + + return true; + } + + + /** + * Create an RM Constraint value + * @throws Exception + */ + public void testGetRMConstraintValue() throws Exception + { + + String constraintName = null; + + /* + * Create a new list + */ + { + String title = "Get Constraint Value"; + JSONArray array = new JSONArray(); + array.put("POTATO"); + array.put("CARROT"); + array.put("TURNIP"); + + JSONObject obj = new JSONObject(); + obj.put("allowedValues", array); + obj.put("constraintTitle", title); + /** + * Now do a post to create a new list + */ + Response response = sendRequest(new PostRequest(URL_RM_CONSTRAINTS, obj.toString(), "application/json"), Status.STATUS_OK); + JSONObject top = new JSONObject(response.getContentAsString()); + + JSONObject data = top.getJSONObject("data"); + constraintName = data.getString("constraintName"); + JSONArray allowedValues = data.getJSONArray("allowedValues"); + assertTrue("values not correct", compare(array, allowedValues)); + } + + /** + * Get the CARROT value + */ + { + String url = URL_RM_CONSTRAINTS + "/" + constraintName + "/values/" + "CARROT"; + Response response = sendRequest(new GetRequest(url), Status.STATUS_OK); + JSONObject top = new JSONObject(response.getContentAsString()); + } + + { + String url = URL_RM_CONSTRAINTS + "/" + constraintName + "/values/" + "ONION"; + sendRequest(new GetRequest(url), Status.STATUS_NOT_FOUND); + } + } +} + diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RMConstraintScriptTest.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RMConstraintScriptTest.java new file mode 100644 index 0000000000..b3cdd47bbc --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RMConstraintScriptTest.java @@ -0,0 +1,164 @@ +/* + * 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.module.org_alfresco_module_rm.test.webscript; + +import java.util.ArrayList; +import java.util.List; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.caveat.RMCaveatConfigService; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.web.scripts.BaseWebScriptTest; +import org.alfresco.service.cmr.security.MutableAuthenticationService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.util.PropertyMap; +import org.json.JSONArray; +import org.json.JSONObject; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.TestWebScriptServer.GetRequest; +import org.springframework.extensions.webscripts.TestWebScriptServer.Response; + +/** + * Test of GET RM Constraint (User facing scripts) + * + * @author Mark Rogers + */ +public class RMConstraintScriptTest extends BaseWebScriptTest +{ + private MutableAuthenticationService authenticationService; + private RMCaveatConfigService caveatConfigService; + private PersonService personService; + + protected final static String RM_LIST = "rmc:smListTest"; + protected final static String RM_LIST_URI_ELEM = "rmc_smListTest"; + + private static final String URL_RM_CONSTRAINTS = "/api/rma/rmconstraints"; + + @Override + protected void setUp() throws Exception + { + this.caveatConfigService = (RMCaveatConfigService)getServer().getApplicationContext().getBean("CaveatConfigService"); + this.authenticationService = (MutableAuthenticationService)getServer().getApplicationContext().getBean("AuthenticationService"); + this.personService = (PersonService)getServer().getApplicationContext().getBean("PersonService"); + super.setUp(); + } + + @Override + protected void tearDown() throws Exception + { + super.tearDown(); + } + + /** + * + * @throws Exception + */ + public void testGetRMConstraint() throws Exception + { + // Set the current security context as admin + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + + /** + * Delete the list to remove any junk then recreate it. + */ + if (caveatConfigService.getRMConstraint(RM_LIST) != null) + { + caveatConfigService.deleteRMConstraint(RM_LIST); + } + caveatConfigService.addRMConstraint(RM_LIST, "my title", new String[0]); + + + createUser("fbloggs"); + createUser("jrogers"); + createUser("jdoe"); + + + List values = new ArrayList(); + values.add("NOFORN"); + values.add("FGI"); + caveatConfigService.updateRMConstraintListAuthority(RM_LIST, "fbloggs", values); + caveatConfigService.updateRMConstraintListAuthority(RM_LIST, "jrogers", values); + caveatConfigService.updateRMConstraintListAuthority(RM_LIST, "jdoe", values); + + AuthenticationUtil.setFullyAuthenticatedUser("jdoe"); + /** + * Positive test Get the constraint + */ + { + String url = URL_RM_CONSTRAINTS + "/" + RM_LIST_URI_ELEM; + Response response = sendRequest(new GetRequest(url), Status.STATUS_OK); + JSONObject top = new JSONObject(response.getContentAsString()); + + JSONObject data = top.getJSONObject("data"); + System.out.println(response.getContentAsString()); + + JSONArray allowedValues = data.getJSONArray("allowedValuesForCurrentUser"); + +// assertTrue("values not correct", compare(array, allowedValues)); + +// JSONArray constraintDetails = data.getJSONArray("constraintDetails"); +// +// assertTrue("details array does not contain 3 elements", constraintDetails.length() == 3); +// for(int i =0; i < constraintDetails.length(); i++) +// { +// JSONObject detail = constraintDetails.getJSONObject(i); +// } + } + + /** + * + * @throws Exception + */ + +// /** +// * Negative test - Attempt to get a constraint that does exist +// */ +// { +// String url = URL_RM_CONSTRAINTS + "/" + "rmc_wibble"; +// sendRequest(new GetRequest(url), Status.STATUS_NOT_FOUND); +// } +// + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + personService.deletePerson("fbloggs"); + personService.deletePerson("jrogers"); + personService.deletePerson("jdoe"); + + } + + private void createUser(String userName) + { + if (this.authenticationService.authenticationExists(userName) == false) + { + this.authenticationService.createAuthentication(userName, "PWD".toCharArray()); + + PropertyMap ppOne = new PropertyMap(4); + ppOne.put(ContentModel.PROP_USERNAME, userName); + ppOne.put(ContentModel.PROP_AUTHORITY_DISPLAY_NAME, "title" + userName); + ppOne.put(ContentModel.PROP_FIRSTNAME, "firstName"); + ppOne.put(ContentModel.PROP_LASTNAME, "lastName"); + ppOne.put(ContentModel.PROP_EMAIL, "email@email.com"); + ppOne.put(ContentModel.PROP_JOBTITLE, "jobTitle"); + + this.personService.createPerson(ppOne); + } + } +} + + + \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RmRestApiTest.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RmRestApiTest.java new file mode 100644 index 0000000000..8c3e0e27f6 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RmRestApiTest.java @@ -0,0 +1,1604 @@ +/* + * 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.module.org_alfresco_module_rm.test.webscript; + +import java.io.IOException; +import java.io.Serializable; +import java.io.UnsupportedEncodingException; +import java.text.MessageFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.transaction.UserTransaction; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminService; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminServiceImpl; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService; +import org.alfresco.module.org_alfresco_module_rm.action.impl.CompleteEventAction; +import org.alfresco.module.org_alfresco_module_rm.audit.RecordsManagementAuditService; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionAction; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementCustomModel; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.module.org_alfresco_module_rm.script.CustomReferenceType; +import org.alfresco.module.org_alfresco_module_rm.test.util.TestActionParams; +import org.alfresco.module.org_alfresco_module_rm.test.util.TestUtilities; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.web.scripts.BaseWebScriptTest; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.dictionary.AspectDefinition; +import org.alfresco.service.cmr.dictionary.AssociationDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +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; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.cmr.view.ImporterService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.alfresco.service.transaction.TransactionService; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONStringer; +import org.json.JSONTokener; +import org.springframework.extensions.surf.util.ISO8601DateFormat; +import org.springframework.extensions.webscripts.TestWebScriptServer.DeleteRequest; +import org.springframework.extensions.webscripts.TestWebScriptServer.GetRequest; +import org.springframework.extensions.webscripts.TestWebScriptServer.PostRequest; +import org.springframework.extensions.webscripts.TestWebScriptServer.PutRequest; +import org.springframework.extensions.webscripts.TestWebScriptServer.Response; + +/** + * This class tests the Rest API for RM. + * + * @author Neil McErlean + */ +public class RmRestApiTest extends BaseWebScriptTest implements RecordsManagementModel +{ + protected static final String GET_NODE_AUDITLOG_URL_FORMAT = "/api/node/{0}/rmauditlog"; + protected static final String GET_TRANSFER_URL_FORMAT = "/api/node/{0}/transfers/{1}"; + protected static final String TRANSFER_REPORT_URL_FORMAT = "/api/node/{0}/transfers/{1}/report"; + protected static final String REF_INSTANCES_URL_FORMAT = "/api/node/{0}/customreferences"; + protected static final String RMA_AUDITLOG_URL = "/api/rma/admin/rmauditlog"; + protected static final String RMA_AUDITLOG_STATUS_URL = "/api/rma/admin/rmauditlog/status"; + protected static final String GET_LIST_URL = "/api/rma/admin/listofvalues"; + protected static final String RMA_ACTIONS_URL = "/api/rma/actions/ExecutionQueue"; + protected static final String APPLICATION_JSON = "application/json"; + protected static final String RMA_CUSTOM_PROPS_DEFINITIONS_URL = "/api/rma/admin/custompropertydefinitions"; + protected static final String RMA_CUSTOM_REFS_DEFINITIONS_URL = "/api/rma/admin/customreferencedefinitions"; + protected NamespaceService namespaceService; + protected NodeService nodeService; + protected ContentService contentService; + protected DictionaryService dictionaryService; + protected SearchService searchService; + protected ImporterService importService; + protected TransactionService transactionService; + protected ServiceRegistry services; + protected RecordsManagementService rmService; + protected RecordsManagementActionService rmActionService; + protected RecordsManagementAuditService rmAuditService; + protected RecordsManagementAdminService rmAdminService; + protected RetryingTransactionHelper transactionHelper; + protected DispositionService dispositionService; + + private static final String BI_DI = "BiDi"; + private static final String CHILD_SRC = "childSrc"; + private static final String CHILD_TGT = "childTgt"; + + @Override + protected void setUp() throws Exception + { + setCustomContext("classpath:org/alfresco/module/org_alfresco_module_rm/test/util/test-context.xml"); + + super.setUp(); + this.namespaceService = (NamespaceService) getServer().getApplicationContext().getBean("NamespaceService"); + this.nodeService = (NodeService) getServer().getApplicationContext().getBean("NodeService"); + this.contentService = (ContentService)getServer().getApplicationContext().getBean("ContentService"); + this.dictionaryService = (DictionaryService)getServer().getApplicationContext().getBean("DictionaryService"); + this.searchService = (SearchService)getServer().getApplicationContext().getBean("SearchService"); + this.importService = (ImporterService)getServer().getApplicationContext().getBean("ImporterService"); + this.transactionService = (TransactionService)getServer().getApplicationContext().getBean("TransactionService"); + this.services = (ServiceRegistry)getServer().getApplicationContext().getBean("ServiceRegistry"); + this.rmService = (RecordsManagementService)getServer().getApplicationContext().getBean("RecordsManagementService"); + this.rmActionService = (RecordsManagementActionService)getServer().getApplicationContext().getBean("RecordsManagementActionService"); + this.rmAuditService = (RecordsManagementAuditService)getServer().getApplicationContext().getBean("RecordsManagementAuditService"); + this.rmAdminService = (RecordsManagementAdminService)getServer().getApplicationContext().getBean("RecordsManagementAdminService"); + transactionHelper = (RetryingTransactionHelper)getServer().getApplicationContext().getBean("retryingTransactionHelper"); + dispositionService = (DispositionService)getServer().getApplicationContext().getBean("DispositionService"); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + + // Bring the filePlan into the test database. + // + // This is quite a slow call, so if this class grew to have many test methods, + // there would be a real benefit in using something like @BeforeClass for the line below. + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public NodeRef execute() throws Throwable + { + return TestUtilities.loadFilePlanData(getServer().getApplicationContext()); + } + }); + } + + /** + * This test method ensures that a POST of an RM action to a non-existent node + * will result in a 404 status. + * + * @throws Exception + */ + // TODO taken out for now + public void xtestPostActionToNonExistentNode() throws Exception + { + NodeRef recordCategory = TestUtilities.getRecordCategory(searchService, "Reports", "AIS Audit Records"); + assertNotNull(recordCategory); + + NodeRef nonExistentNode = new NodeRef("workspace://SpacesStore/09ca1e02-1c87-4a53-97e7-xxxxxxxxxxxx"); + + // Construct the JSON request. + JSONObject jsonPostData = new JSONObject(); + jsonPostData.put("nodeRef", nonExistentNode.toString()); + // Although the request specifies a 'reviewed' action, it does not matter what + // action is specified here, as the non-existent Node should trigger a 404 + // before the action is executed. + jsonPostData.put("name", "reviewed"); + + // Submit the JSON request. + String jsonPostString = jsonPostData.toString(); + + final int expectedStatus = 404; + sendRequest(new PostRequest(RMA_ACTIONS_URL, jsonPostString, APPLICATION_JSON), expectedStatus); + } + + public void testPostReviewedAction() throws IOException, JSONException + { + // Get the recordCategory under which we will create the testNode. + NodeRef recordCategory = TestUtilities.getRecordCategory(searchService, "Reports", "AIS Audit Records"); + assertNotNull(recordCategory); + + NodeRef recordFolder = TestUtilities.getRecordFolder(searchService, "Reports", "AIS Audit Records", "January AIS Audit Records"); + assertNotNull(recordFolder); + + // Create a testNode/file which is to be declared as a record. + NodeRef testRecord = this.nodeService.createNode(recordFolder, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "MyRecord.txt"), + ContentModel.TYPE_CONTENT).getChildRef(); + + // Set some dummy content. + ContentWriter writer = this.contentService.getWriter(testRecord, ContentModel.PROP_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.setEncoding("UTF-8"); + writer.putContent("There is some content in this record"); + + // In this test, this property has a date-value equal to the model import time. + Serializable pristineReviewAsOf = this.nodeService.getProperty(testRecord, PROP_REVIEW_AS_OF); + + // Construct the JSON request for 'reviewed'. + String jsonString = new JSONStringer().object() + .key("name").value("reviewed") + .key("nodeRef").value(testRecord.toString()) + // These JSON params are just to test the submission of params. They'll be ignored. + .key("params").object() + .key("param1").value("one") + .key("param2").value("two") + .endObject() + .endObject() + .toString(); + + // Submit the JSON request. + final int expectedStatus = 200; + Response rsp = sendRequest(new PostRequest(RMA_ACTIONS_URL, + jsonString, APPLICATION_JSON), expectedStatus); + + String rspContent = rsp.getContentAsString(); + assertTrue(rspContent.contains("Successfully queued action [reviewed]")); + + Serializable newReviewAsOfDate = this.nodeService.getProperty(testRecord, PROP_REVIEW_AS_OF); + assertFalse("The reviewAsOf property should have changed. Was " + pristineReviewAsOf, + pristineReviewAsOf.equals(newReviewAsOfDate)); + } + + public void testPostMultiReviewedAction() throws IOException, JSONException + { + // Get the recordCategory under which we will create the testNode. + NodeRef recordCategory = TestUtilities.getRecordCategory(searchService, "Reports", "AIS Audit Records"); + assertNotNull(recordCategory); + + NodeRef recordFolder = TestUtilities.getRecordFolder(searchService, "Reports", "AIS Audit Records", "January AIS Audit Records"); + assertNotNull(recordFolder); + + // Create a testNode/file which is to be declared as a record. + NodeRef testRecord = this.nodeService.createNode(recordFolder, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "MyRecord.txt"), + ContentModel.TYPE_CONTENT).getChildRef(); + + // Set some dummy content. + ContentWriter writer = this.contentService.getWriter(testRecord, ContentModel.PROP_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.setEncoding("UTF-8"); + writer.putContent("There is some content in this record"); + + NodeRef testRecord2 = this.nodeService.createNode(recordFolder, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "MyRecord2.txt"), + ContentModel.TYPE_CONTENT).getChildRef(); + + // Set some dummy content. + writer = this.contentService.getWriter(testRecord2, ContentModel.PROP_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.setEncoding("UTF-8"); + writer.putContent("There is some content in this record"); + + NodeRef testRecord3 = this.nodeService.createNode(recordFolder, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "MyRecord3.txt"), + ContentModel.TYPE_CONTENT).getChildRef(); + + // Set some dummy content. + writer = this.contentService.getWriter(testRecord3, ContentModel.PROP_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.setEncoding("UTF-8"); + writer.putContent("There is some content in this record"); + + // In this test, this property has a date-value equal to the model import time. + Serializable pristineReviewAsOf = this.nodeService.getProperty(testRecord, PROP_REVIEW_AS_OF); + Serializable pristineReviewAsOf2 = this.nodeService.getProperty(testRecord2, PROP_REVIEW_AS_OF); + Serializable pristineReviewAsOf3 = this.nodeService.getProperty(testRecord3, PROP_REVIEW_AS_OF); + + // Construct the JSON request for 'reviewed'. + String jsonString = new JSONStringer().object() + .key("name").value("reviewed") + .key("nodeRefs").array() + .value(testRecord.toString()) + .value(testRecord2.toString()) + .value(testRecord3.toString()) + .endArray() + // These JSON params are just to test the submission of params. They'll be ignored. + .key("params").object() + .key("param1").value("one") + .key("param2").value("two") + .endObject() + .endObject() + .toString(); + + // Submit the JSON request. + final int expectedStatus = 200; + Response rsp = sendRequest(new PostRequest(RMA_ACTIONS_URL, + jsonString, APPLICATION_JSON), expectedStatus); + + String rspContent = rsp.getContentAsString(); + assertTrue(rspContent.contains("Successfully queued action [reviewed]")); + + Serializable newReviewAsOfDate = this.nodeService.getProperty(testRecord, PROP_REVIEW_AS_OF); + assertFalse("The reviewAsOf property should have changed. Was " + pristineReviewAsOf, + pristineReviewAsOf.equals(newReviewAsOfDate)); + Serializable newReviewAsOfDate2 = this.nodeService.getProperty(testRecord2, PROP_REVIEW_AS_OF); + assertFalse("The reviewAsOf property should have changed. Was " + pristineReviewAsOf2, + pristineReviewAsOf2.equals(newReviewAsOfDate2)); + Serializable newReviewAsOfDate3 = this.nodeService.getProperty(testRecord3, PROP_REVIEW_AS_OF); + assertFalse("The reviewAsOf property should have changed. Was " + pristineReviewAsOf3, + pristineReviewAsOf3.equals(newReviewAsOfDate3)); + } + + public void testActionParams() throws Exception + { + // Construct the JSON request for 'reviewed'. + String jsonString = new JSONStringer().object() + .key("name").value("testActionParams") + .key("nodeRef").array() + .value("nothing://nothing/nothing") + .endArray() + // These JSON params are just to test the submission of params. They'll be ignored. + .key("params").object() + .key(TestActionParams.PARAM_DATE).object() + .key("iso8601") + .value(ISO8601DateFormat.format(new Date())) + .endObject() + .endObject() + .endObject() + .toString(); + + // Submit the JSON request. + final int expectedStatus = 200; + //TODO Currently failing unit test. +// Response rsp = sendRequest(new PostRequest(RMA_ACTIONS_URL, +// jsonString, APPLICATION_JSON), expectedStatus); + } + + public void testPostCustomReferenceDefinitions() throws IOException, JSONException + { + postCustomReferenceDefinitions(); + } + + /** + * This method creates a child and a non-child reference and returns their generated ids. + * + * + * @return String[] with element 0 = refId of p/c ref, 1 = refId pf bidi. + */ + private String[] postCustomReferenceDefinitions() throws JSONException, IOException, + UnsupportedEncodingException { + String[] result = new String[2]; + + // 1. Child association. + String jsonString = new JSONStringer().object() + .key("referenceType").value(CustomReferenceType.PARENT_CHILD) + .key("source").value(CHILD_SRC) + .key("target").value(CHILD_TGT) + .endObject() + .toString(); + +// System.out.println(jsonString); + + // Submit the JSON request. + final int expectedStatus = 200; + Response rsp = sendRequest(new PostRequest(RMA_CUSTOM_REFS_DEFINITIONS_URL, + jsonString, APPLICATION_JSON), expectedStatus); + + String rspContent = rsp.getContentAsString(); + assertTrue(rspContent.contains("success")); + +// System.out.println(rspContent); + + JSONObject jsonRsp = new JSONObject(new JSONTokener(rspContent)); + String generatedChildRefId = jsonRsp.getJSONObject("data").getString("refId"); + result[0] = generatedChildRefId; + + // 2. Non-child or standard association. + jsonString = new JSONStringer().object() + .key("referenceType").value(CustomReferenceType.BIDIRECTIONAL) + .key("label").value(BI_DI) + .endObject() + .toString(); + +// System.out.println(jsonString); + + // Submit the JSON request. + rsp = sendRequest(new PostRequest(RMA_CUSTOM_REFS_DEFINITIONS_URL, + jsonString, APPLICATION_JSON), expectedStatus); + + rspContent = rsp.getContentAsString(); + assertTrue(rspContent.contains("success")); + +// System.out.println(rspContent); + + jsonRsp = new JSONObject(new JSONTokener(rspContent)); + String generatedBidiRefId = jsonRsp.getJSONObject("data").getString("refId"); + result[1] = generatedBidiRefId; + + // Now assert that both have appeared in the data dictionary. + AspectDefinition customAssocsAspect = + dictionaryService.getAspect(QName.createQName(RecordsManagementAdminServiceImpl.RMC_CUSTOM_ASSOCS, namespaceService)); + assertNotNull("Missing customAssocs aspect", customAssocsAspect); + + QName newRefQname = rmAdminService.getQNameForClientId(generatedChildRefId); + Map associations = customAssocsAspect.getAssociations(); + assertTrue("Custom child assoc not returned by dataDictionary.", associations.containsKey(newRefQname)); + + newRefQname = rmAdminService.getQNameForClientId(generatedBidiRefId); + assertTrue("Custom std assoc not returned by dataDictionary.", customAssocsAspect.getAssociations().containsKey(newRefQname)); + + return result; + } + + public void testPutCustomPropertyDefinition() throws Exception + { + // POST to create a property definition with a known propId + final String propertyLabel = "Original label åçîéøü"; + String propId = postCustomPropertyDefinition(propertyLabel, null); + + // PUT an updated label. + final String updatedLabel = "Updated label πø^¨¥†®"; + String jsonString = new JSONStringer().object() + .key("label").value(updatedLabel) + .endObject() + .toString(); + + String propDefnUrl = "/api/rma/admin/custompropertydefinitions/" + propId; + Response rsp = sendRequest(new PutRequest(propDefnUrl, + jsonString, APPLICATION_JSON), 200); + + // GET from the URL again to ensure it's valid + rsp = sendRequest(new GetRequest(propDefnUrl), 200); + String rspContent = rsp.getContentAsString(); + +// System.out.println(rspContent); + + // PUT an updated constraint ref. + final String updatedConstraint = "rmc:tlList"; + jsonString = new JSONStringer().object() + .key("constraintRef").value(updatedConstraint) + .endObject() + .toString(); + + propDefnUrl = "/api/rma/admin/custompropertydefinitions/" + propId; + rsp = sendRequest(new PutRequest(propDefnUrl, + jsonString, APPLICATION_JSON), 200); + + rspContent = rsp.getContentAsString(); + + JSONObject jsonRsp = new JSONObject(new JSONTokener(rspContent)); + String urlOfNewPropDef = jsonRsp.getString("url"); + assertNotNull("urlOfNewPropDef was null.", urlOfNewPropDef); + + // GET from the URL again to ensure it's valid + rsp = sendRequest(new GetRequest(propDefnUrl), 200); + rspContent = rsp.getContentAsString(); + +// System.out.println(rspContent); + + jsonRsp = new JSONObject(new JSONTokener(rspContent)); + JSONObject dataObject = jsonRsp.getJSONObject("data"); + assertNotNull("JSON data object was null", dataObject); + JSONObject customPropsObject = dataObject.getJSONObject("customProperties"); + assertNotNull("JSON customProperties object was null", customPropsObject); + assertEquals("Wrong customProperties length.", 1, customPropsObject.length()); + + Object keyToSoleProp = customPropsObject.keys().next(); + + JSONObject newPropObject = customPropsObject.getJSONObject((String)keyToSoleProp); + assertEquals("Wrong property label.", updatedLabel, newPropObject.getString("label")); + JSONArray constraintRefsArray = newPropObject.getJSONArray("constraintRefs"); + assertEquals("ConstraintRefsArray wrong length.", 1, constraintRefsArray.length()); + String retrievedUpdatedTitle = constraintRefsArray.getJSONObject(0).getString("name"); + assertEquals("Constraints had wrong name.", "rmc:tlList", retrievedUpdatedTitle); + + // PUT again to remove all constraints + jsonString = new JSONStringer().object() + .key("constraintRef").value(null) + .endObject() + .toString(); + + rsp = sendRequest(new PutRequest(propDefnUrl, + jsonString, APPLICATION_JSON), 200); + + rspContent = rsp.getContentAsString(); +// System.out.println(rspContent); + + jsonRsp = new JSONObject(new JSONTokener(rspContent)); + + // GET from the URL again to ensure it's valid + rsp = sendRequest(new GetRequest(propDefnUrl), 200); + rspContent = rsp.getContentAsString(); + +// System.out.println(rspContent); + + jsonRsp = new JSONObject(new JSONTokener(rspContent)); + dataObject = jsonRsp.getJSONObject("data"); + assertNotNull("JSON data object was null", dataObject); + customPropsObject = dataObject.getJSONObject("customProperties"); + assertNotNull("JSON customProperties object was null", customPropsObject); + assertEquals("Wrong customProperties length.", 1, customPropsObject.length()); + + keyToSoleProp = customPropsObject.keys().next(); + + newPropObject = customPropsObject.getJSONObject((String)keyToSoleProp); + assertEquals("Wrong property label.", updatedLabel, newPropObject.getString("label")); + constraintRefsArray = newPropObject.getJSONArray("constraintRefs"); + assertEquals("ConstraintRefsArray wrong length.", 0, constraintRefsArray.length()); + + // Finally PUT a constraint on a PropertyDefn that has been cleared of constraints. + // This was raised as an issue + final String readdedConstraint = "rmc:tlList"; + jsonString = new JSONStringer().object() + .key("constraintRef").value(readdedConstraint) + .endObject() + .toString(); + + propDefnUrl = "/api/rma/admin/custompropertydefinitions/" + propId; + rsp = sendRequest(new PutRequest(propDefnUrl, + jsonString, APPLICATION_JSON), 200); + + rspContent = rsp.getContentAsString(); + + jsonRsp = new JSONObject(new JSONTokener(rspContent)); +// System.out.println("PUTting a constraint back again."); +// System.out.println(rspContent); + + // And GET from the URL again + rsp = sendRequest(new GetRequest(propDefnUrl), 200); + rspContent = rsp.getContentAsString(); + +// System.out.println(rspContent); + + jsonRsp = new JSONObject(new JSONTokener(rspContent)); + dataObject = jsonRsp.getJSONObject("data"); + assertNotNull("JSON data object was null", dataObject); + customPropsObject = dataObject.getJSONObject("customProperties"); + assertNotNull("JSON customProperties object was null", customPropsObject); + assertEquals("Wrong customProperties length.", 1, customPropsObject.length()); + + keyToSoleProp = customPropsObject.keys().next(); + + newPropObject = customPropsObject.getJSONObject((String)keyToSoleProp); + assertEquals("Wrong property label.", updatedLabel, newPropObject.getString("label")); + constraintRefsArray = newPropObject.getJSONArray("constraintRefs"); + assertEquals("ConstraintRefsArray wrong length.", 1, constraintRefsArray.length()); + String readdedUpdatedTitle = constraintRefsArray.getJSONObject(0).getString("name"); + assertEquals("Constraints had wrong name.", "rmc:tlList", readdedUpdatedTitle); + } + + public void testGetCustomReferences() throws IOException, JSONException + { + // Ensure that there is at least one custom reference. + postCustomReferenceDefinitions(); + + // GET all custom reference definitions + final int expectedStatus = 200; + Response rsp = sendRequest(new GetRequest(RMA_CUSTOM_REFS_DEFINITIONS_URL), expectedStatus); + + JSONObject jsonRsp = new JSONObject(new JSONTokener(rsp.getContentAsString())); + + JSONObject dataObj = (JSONObject)jsonRsp.get("data"); + assertNotNull("JSON 'data' object was null", dataObj); + + JSONArray customRefsObj = (JSONArray)dataObj.get("customReferences"); + assertNotNull("JSON 'customReferences' object was null", customRefsObj); + +// for (int i = 0; i < customRefsObj.length(); i++) { +// System.out.println(customRefsObj.getString(i)); +// } + + assertTrue("There should be at least two custom references. Found " + customRefsObj, customRefsObj.length() >= 2); + + // GET a specific custom reference definition. + // Here, we're using one of the built-in references + // qname = rmc:versions + rsp = sendRequest(new GetRequest(RMA_CUSTOM_REFS_DEFINITIONS_URL + "/" + "versions"), expectedStatus); + + jsonRsp = new JSONObject(new JSONTokener(rsp.getContentAsString())); + + dataObj = (JSONObject)jsonRsp.get("data"); + assertNotNull("JSON 'data' object was null", dataObj); + + customRefsObj = (JSONArray)dataObj.get("customReferences"); + assertNotNull("JSON 'customProperties' object was null", customRefsObj); + + assertTrue("There should be exactly 1 custom references. Found " + customRefsObj.length(), customRefsObj.length() == 1); + } + + public void testGetDodCustomTypes() throws IOException, JSONException + { + final int expectedStatus = 200; + Response rsp = sendRequest(new GetRequest("/api/rma/admin/dodcustomtypes"), expectedStatus); + + String rspContent = rsp.getContentAsString(); + JSONObject jsonRsp = new JSONObject(new JSONTokener(rspContent)); + + // System.out.println(rspContent); + + JSONObject dataObj = (JSONObject)jsonRsp.get("data"); + assertNotNull("JSON 'data' object was null", dataObj); + + JSONArray customTypesObj = (JSONArray)dataObj.get("dodCustomTypes"); + assertNotNull("JSON 'dodCustomTypes' object was null", customTypesObj); + + assertEquals("Wrong DOD custom types count.", 4, customTypesObj.length()); + } + + public void testGetPostAndRemoveCustomReferenceInstances() throws Exception + { + // Create test records. + NodeRef recordFolder = retrievePreexistingRecordFolder(); + NodeRef testRecord1 = createRecord(recordFolder, "testRecord1" + System.currentTimeMillis(), "The from recørd"); + NodeRef testRecord2 = createRecord(recordFolder, "testRecord2" + System.currentTimeMillis(), "The to récord"); + + String node1Url = testRecord1.toString().replace("://", "/"); + String refInstancesRecord1Url = MessageFormat.format(REF_INSTANCES_URL_FORMAT, node1Url); + + // Create reference types. + String[] generatedRefIds = postCustomReferenceDefinitions(); + + // Add a standard ref + String jsonString = new JSONStringer().object() + .key("toNode").value(testRecord2.toString()) + .key("refId").value(generatedRefIds[1]) + .endObject() + .toString(); + + Response rsp = sendRequest(new PostRequest(refInstancesRecord1Url, + jsonString, APPLICATION_JSON), 200); + + // Add a child ref + jsonString = new JSONStringer().object() + .key("toNode").value(testRecord2.toString()) + .key("refId").value(generatedRefIds[0]) + .endObject() + .toString(); + +// System.out.println(jsonString); + + rsp = sendRequest(new PostRequest(refInstancesRecord1Url, + jsonString, APPLICATION_JSON), 200); + +// System.out.println(rsp.getContentAsString()); + + // Now retrieve the applied references from the REST API + // 1. references on the 'from' record. + rsp = sendRequest(new GetRequest(refInstancesRecord1Url), 200); + + String contentAsString = rsp.getContentAsString(); +// System.out.println(contentAsString); + + JSONObject jsonRsp = new JSONObject(new JSONTokener(contentAsString)); + + JSONObject dataObj = (JSONObject)jsonRsp.get("data"); + assertNotNull("JSON 'data' object was null", dataObj); + + JSONArray customRefsFromArray = (JSONArray)dataObj.get("customReferencesFrom"); + assertNotNull("JSON 'customReferencesFrom' object was null", customRefsFromArray); + + int customRefsCount = customRefsFromArray.length(); + assertTrue("There should be at least one custom reference. Found " + customRefsFromArray, customRefsCount > 0); + + JSONArray customRefsToArray = (JSONArray)dataObj.get("customReferencesTo"); + assertNotNull("JSON 'customReferencesTo' object was null", customRefsToArray); + assertEquals("customReferencesTo wrong length.", 0, customRefsToArray.length()); + + // 2. Back-references on the 'to' record + String node2Url = testRecord2.toString().replace("://", "/"); + String refInstancesRecord2Url = MessageFormat.format(REF_INSTANCES_URL_FORMAT, node2Url); + + rsp = sendRequest(new GetRequest(refInstancesRecord2Url), 200); + + contentAsString = rsp.getContentAsString(); + + jsonRsp = new JSONObject(new JSONTokener(contentAsString)); + + dataObj = (JSONObject)jsonRsp.get("data"); + assertNotNull("JSON 'data' object was null", dataObj); + + customRefsToArray = (JSONArray)dataObj.get("customReferencesTo"); + assertNotNull("JSON 'customReferencesTo' object was null", customRefsToArray); + + customRefsCount = customRefsToArray.length(); + assertTrue("There should be at least one custom reference. Found " + customRefsToArray, customRefsCount > 0); + + customRefsFromArray = (JSONArray)dataObj.get("customReferencesFrom"); + assertNotNull("JSON 'customReferencesFrom' object was null", customRefsFromArray); + assertEquals("customReferencesFrom wrong length.", 0, customRefsFromArray.length()); + + + + // Now to delete a reference instance of each type + String protocol = testRecord2.getStoreRef().getProtocol(); + String identifier = testRecord2.getStoreRef().getIdentifier(); + String recId = testRecord2.getId(); + final String queryFormat = "?st={0}&si={1}&id={2}"; + String urlQueryString = MessageFormat.format(queryFormat, protocol, identifier, recId); + + rsp = sendRequest(new DeleteRequest(refInstancesRecord1Url + "/" + generatedRefIds[1] + urlQueryString), 200); + assertTrue(rsp.getContentAsString().contains("success")); + + rsp = sendRequest(new DeleteRequest(refInstancesRecord1Url + "/" + + generatedRefIds[0] + + urlQueryString), 200); + assertTrue(rsp.getContentAsString().contains("success")); + + // Get the reference instances back and confirm they've been removed. + rsp = sendRequest(new GetRequest(refInstancesRecord1Url), 200); + + jsonRsp = new JSONObject(new JSONTokener(rsp.getContentAsString())); + + dataObj = (JSONObject)jsonRsp.get("data"); + assertNotNull("JSON 'data' object was null", dataObj); + + customRefsFromArray = (JSONArray)dataObj.get("customReferencesFrom"); + assertNotNull("JSON 'customReferences' object was null", customRefsFromArray); + assertTrue("customRefsArray was unexpectedly not empty.", customRefsFromArray.length() == 0); + } + + public void testMob1630ShouldNotBeAbleToCreateTwoSupersedesReferencesOnOneRecordPair() throws Exception + { + // Create 2 test records. + NodeRef recordFolder = retrievePreexistingRecordFolder(); + NodeRef testRecord1 = createRecord(recordFolder, "testRecord1" + System.currentTimeMillis(), "The from recørd"); + NodeRef testRecord2 = createRecord(recordFolder, "testRecord2" + System.currentTimeMillis(), "The to récord"); + + String node1Url = testRecord1.toString().replace("://", "/"); + String node2Url = testRecord2.toString().replace("://", "/"); + String refInstancesRecord1Url = MessageFormat.format(REF_INSTANCES_URL_FORMAT, node1Url); + String refInstancesRecord2Url = MessageFormat.format(REF_INSTANCES_URL_FORMAT, node2Url); + + {// Sanity check. There should be no references defined on these new records. + Response rsp = sendRequest(new GetRequest(refInstancesRecord1Url), 200); + + String rspContent = rsp.getContentAsString(); + JSONObject jsonRsp = new JSONObject(new JSONTokener(rspContent)); + JSONObject dataObj = jsonRsp.getJSONObject("data"); + JSONArray refsFrom = dataObj.getJSONArray("customReferencesFrom"); + JSONArray refsTo = dataObj.getJSONArray("customReferencesTo"); + assertEquals("Incorrect from-refs count.", 0, refsFrom.length()); + assertEquals("Incorrect to-refs count.", 0, refsTo.length()); + } + + // Add a supersedes ref instance between them + final String supersedesRefLocalName = "supersedes"; + String jsonString = new JSONStringer().object() + .key("toNode").value(testRecord2.toString()) + .key("refId").value(supersedesRefLocalName) + .endObject() + .toString(); + + Response rsp = sendRequest(new PostRequest(refInstancesRecord1Url, + jsonString, APPLICATION_JSON), 200); + + // The bug is that we can apply two such references which should not be allowed + rsp = sendRequest(new PostRequest(refInstancesRecord1Url, + jsonString, APPLICATION_JSON), 500); + + {// Retrieve reference instances on this pair of records. + // The first record + rsp = sendRequest(new GetRequest(refInstancesRecord1Url), 200); + + String rspContent = rsp.getContentAsString(); + JSONObject jsonRsp = new JSONObject(new JSONTokener(rspContent)); + JSONObject dataObj = jsonRsp.getJSONObject("data"); + JSONArray refsFrom = dataObj.getJSONArray("customReferencesFrom"); + JSONArray refsTo = dataObj.getJSONArray("customReferencesTo"); + assertEquals("Incorrect from-refs count.", 1, refsFrom.length()); + assertEquals("Incorrect to-refs count.", 0, refsTo.length()); + + // The second record - the back-reference + rsp = sendRequest(new GetRequest(refInstancesRecord2Url), 200); + + rspContent = rsp.getContentAsString(); + jsonRsp = new JSONObject(new JSONTokener(rspContent)); + dataObj = jsonRsp.getJSONObject("data"); + refsFrom = dataObj.getJSONArray("customReferencesFrom"); + refsTo = dataObj.getJSONArray("customReferencesTo"); + assertEquals("Incorrect from-refs count.", 0, refsFrom.length()); + assertEquals("Incorrect to-refs count.", 1, refsTo.length()); + } + + // Delete the reference instance + String protocol = testRecord2.getStoreRef().getProtocol(); + String identifier = testRecord2.getStoreRef().getIdentifier(); + String recId = testRecord2.getId(); + final String queryFormat = "?st={0}&si={1}&id={2}"; + String urlQueryString = MessageFormat.format(queryFormat, protocol, identifier, recId); + + rsp = sendRequest(new DeleteRequest(refInstancesRecord1Url + "/" + supersedesRefLocalName + urlQueryString), 200); + assertTrue(rsp.getContentAsString().contains("success")); + + {// Retrieve reference instances on this pair of records. + // The first record + rsp = sendRequest(new GetRequest(refInstancesRecord1Url), 200); + + String rspContent = rsp.getContentAsString(); + JSONObject jsonRsp = new JSONObject(new JSONTokener(rspContent)); + JSONObject dataObj = jsonRsp.getJSONObject("data"); + JSONArray refsFrom = dataObj.getJSONArray("customReferencesFrom"); + JSONArray refsTo = dataObj.getJSONArray("customReferencesTo"); + assertEquals("Incorrect from-refs count.", 0, refsFrom.length()); + assertEquals("Incorrect to-refs count.", 0, refsTo.length()); + + // The second record - the back-reference + rsp = sendRequest(new GetRequest(refInstancesRecord2Url), 200); + + rspContent = rsp.getContentAsString(); + jsonRsp = new JSONObject(new JSONTokener(rspContent)); + dataObj = jsonRsp.getJSONObject("data"); + refsFrom = dataObj.getJSONArray("customReferencesFrom"); + refsTo = dataObj.getJSONArray("customReferencesTo"); + assertEquals("Incorrect from-refs count.", 0, refsFrom.length()); + assertEquals("Incorrect to-refs count.", 0, refsTo.length()); + } + } + + public void testPostCustomPropertyDefinition() throws Exception + { + long currentTimeMillis = System.currentTimeMillis(); + + // Create one with no propId - it'll get generated. + postCustomPropertyDefinition("customProperty" + currentTimeMillis, null); + + // Create another with an explicit propId. + postCustomPropertyDefinition("customProperty" + currentTimeMillis, "prop" + currentTimeMillis); + } + + /** + * Creates a new property definition using a POST call. + * GETs the resultant property definition. + * + * @param propertyLabel the label to use + * @param propId the propId to use - null to have one generated. + * @return the propId of the new property definition + */ + private String postCustomPropertyDefinition(String propertyLabel, String propId) throws JSONException, + IOException, UnsupportedEncodingException + { + String jsonString; + if (propId == null) + { + jsonString = new JSONStringer().object() + .key("label").value(propertyLabel) + .key("description").value("Dynamically defined test property") + .key("mandatory").value(false) + .key("dataType").value("d:text") + .key("element").value("record") + .key("constraintRef").value("rmc:smList") + // Note no propId + .endObject() + .toString(); + } + else + { + jsonString = new JSONStringer().object() + .key("label").value(propertyLabel) + .key("description").value("Dynamically defined test property") + .key("mandatory").value(false) + .key("dataType").value("d:text") + .key("element").value("record") + .key("constraintRef").value("rmc:smList") + .key("propId").value(propId) + .endObject() + .toString(); + } + + // Submit the JSON request. + final int expectedStatus = 200; + Response rsp = sendRequest(new PostRequest("/api/rma/admin/custompropertydefinitions?element=record", + jsonString, APPLICATION_JSON), expectedStatus); + + String rspContent = rsp.getContentAsString(); + +// System.out.println(rspContent); + + JSONObject jsonRsp = new JSONObject(new JSONTokener(rspContent)); + String urlOfNewPropDef = jsonRsp.getString("url"); + String newPropId = jsonRsp.getString("propId"); + + assertNotNull("urlOfNewPropDef was null.", urlOfNewPropDef); + + // GET from the URL we're given to ensure it's valid + rsp = sendRequest(new GetRequest(urlOfNewPropDef), 200); + rspContent = rsp.getContentAsString(); + +// System.out.println(rspContent); + + jsonRsp = new JSONObject(new JSONTokener(rspContent)); + JSONObject dataObject = jsonRsp.getJSONObject("data"); + assertNotNull("JSON data object was null", dataObject); + JSONObject customPropsObject = dataObject.getJSONObject("customProperties"); + assertNotNull("JSON customProperties object was null", customPropsObject); + assertEquals("Wrong customProperties length.", 1, customPropsObject.length()); + + Object keyToSoleProp = customPropsObject.keys().next(); + +// System.out.println("New property defn: " + keyToSoleProp); + + JSONObject newPropObject = customPropsObject.getJSONObject((String)keyToSoleProp); + assertEquals("Wrong property label.", propertyLabel, newPropObject.getString("label")); + + return newPropId; + } + + public void testPutCustomReferenceDefinition() throws Exception + { + String[] generatedRefIds = postCustomReferenceDefinitions(); + final String pcRefId = generatedRefIds[0]; + final String bidiRefId = generatedRefIds[1]; + + // GET the custom refs in order to retrieve the label/source/target + String refDefnUrl = "/api/rma/admin/customreferencedefinitions/" + bidiRefId; + Response rsp = sendRequest(new GetRequest(refDefnUrl), 200); + + String rspContent = rsp.getContentAsString(); +// System.out.println(rspContent); + JSONObject jsonRsp = new JSONObject(new JSONTokener(rspContent)); + + refDefnUrl = "/api/rma/admin/customreferencedefinitions/" + pcRefId; + rsp = sendRequest(new GetRequest(refDefnUrl), 200); + + rspContent = rsp.getContentAsString(); +// System.out.println(rspContent); + jsonRsp = new JSONObject(new JSONTokener(rspContent)); + + // Update the bidirectional reference. + final String updatedBiDiLabel = "Updated label üøéîçå"; + String jsonString = new JSONStringer().object() + .key("label").value(updatedBiDiLabel) + .endObject() + .toString(); + + refDefnUrl = "/api/rma/admin/customreferencedefinitions/" + bidiRefId; + rsp = sendRequest(new PutRequest(refDefnUrl, + jsonString, APPLICATION_JSON), 200); + + rspContent = rsp.getContentAsString(); +// System.out.println(rspContent); + + jsonRsp = new JSONObject(new JSONTokener(rspContent)); + String urlOfNewRefDef = jsonRsp.getString("url"); + assertNotNull("urlOfNewRefDef was null.", urlOfNewRefDef); + + // GET the bidi reference to ensure it's valid + rsp = sendRequest(new GetRequest(refDefnUrl), 200); + rspContent = rsp.getContentAsString(); + +// System.out.println(rspContent); + + jsonRsp = new JSONObject(new JSONTokener(rspContent)); + JSONObject dataObject = jsonRsp.getJSONObject("data"); + assertNotNull("JSON data object was null", dataObject); + JSONArray customRefsObject = dataObject.getJSONArray("customReferences"); + assertNotNull("JSON customReferences object was null", customRefsObject); + assertEquals("Wrong customReferences length.", 1, customRefsObject.length()); + + JSONObject newRefObject = customRefsObject.getJSONObject(0); + assertEquals("Wrong property label.", updatedBiDiLabel, newRefObject.getString("label")); + + // Update the parent/child reference. + final String updatedPcSource = "Updated source ∆Ωç√∫"; + final String updatedPcTarget = "Updated target ∆Ωç√∫"; + jsonString = new JSONStringer().object() + .key("source").value(updatedPcSource) + .key("target").value(updatedPcTarget) + .endObject() + .toString(); + + refDefnUrl = "/api/rma/admin/customreferencedefinitions/" + pcRefId; + rsp = sendRequest(new PutRequest(refDefnUrl, + jsonString, APPLICATION_JSON), 200); + + rspContent = rsp.getContentAsString(); +// System.out.println(rspContent); + + jsonRsp = new JSONObject(new JSONTokener(rspContent)); + urlOfNewRefDef = jsonRsp.getString("url"); + assertNotNull("urlOfNewRefDef was null.", urlOfNewRefDef); + + // GET the parent/child reference to ensure it's valid + refDefnUrl = "/api/rma/admin/customreferencedefinitions/" + pcRefId; + + rsp = sendRequest(new GetRequest(refDefnUrl), 200); + rspContent = rsp.getContentAsString(); + +// System.out.println(rspContent); + + jsonRsp = new JSONObject(new JSONTokener(rspContent)); + dataObject = jsonRsp.getJSONObject("data"); + assertNotNull("JSON data object was null", dataObject); + customRefsObject = dataObject.getJSONArray("customReferences"); + assertNotNull("JSON customReferences object was null", customRefsObject); + assertEquals("Wrong customReferences length.", 1, customRefsObject.length()); + + newRefObject = customRefsObject.getJSONObject(0); + assertEquals("Wrong reference source.", updatedPcSource, newRefObject.getString("source")); + assertEquals("Wrong reference target.", updatedPcTarget, newRefObject.getString("target")); + } + + public void testGetCustomProperties() throws Exception + { + getCustomProperties(); + } + + private String getCustomProperties() throws Exception, IOException, + UnsupportedEncodingException, JSONException + { + // Ensure that there is at least one custom property. + this.testPostCustomPropertyDefinition(); + + final int expectedStatus = 200; + Response rsp = sendRequest(new GetRequest("/api/rma/admin/custompropertydefinitions?element=record"), expectedStatus); + + String contentAsString = rsp.getContentAsString(); +// System.out.println(contentAsString); + JSONObject jsonRsp = new JSONObject(new JSONTokener(contentAsString)); + + JSONObject dataObj = (JSONObject)jsonRsp.get("data"); + assertNotNull("JSON 'data' object was null", dataObj); + + JSONObject customPropsObj = (JSONObject)dataObj.get("customProperties"); + assertNotNull("JSON 'customProperties' object was null", customPropsObj); + + final int customPropsCount = customPropsObj.length(); + assertTrue("There should be at least one custom property. Found " + customPropsObj, customPropsCount > 0); + + return contentAsString; + } + + public void testGetRecordMetaDataAspects() throws Exception + { + Response rsp = sendRequest(new GetRequest("/api/rma/recordmetadataaspects"), 200); + String contentAsString = rsp.getContentAsString(); + System.out.println(contentAsString); + JSONObject jsonRsp = new JSONObject(new JSONTokener(contentAsString)); + + JSONObject dataObj = (JSONObject)jsonRsp.get("data"); + assertNotNull("JSON 'data' object was null", dataObj); + + JSONArray aspects = dataObj.getJSONArray("recordMetaDataAspects"); + assertNotNull(aspects); + assertEquals(5, aspects.length()); + + // TODO test the items themselves + } + + public void testExport() throws Exception + { + NodeRef recordFolder1 = TestUtilities.getRecordFolder(searchService, "Reports", + "AIS Audit Records", "January AIS Audit Records"); + assertNotNull(recordFolder1); + + NodeRef recordFolder2 = TestUtilities.getRecordFolder(searchService, "Reports", + "Unit Manning Documents", "1st Quarter Unit Manning Documents"); + assertNotNull(recordFolder2); + + String exportUrl = "/api/rma/admin/export"; + + // define JSON POST body + JSONObject jsonPostData = new JSONObject(); + JSONArray nodeRefs = new JSONArray(); + nodeRefs.put(recordFolder1.toString()); + nodeRefs.put(recordFolder2.toString()); + jsonPostData.put("nodeRefs", nodeRefs); + String jsonPostString = jsonPostData.toString(); + + // make the export request + Response rsp = sendRequest(new PostRequest(exportUrl, jsonPostString, APPLICATION_JSON), 200); + assertEquals("application/acp", rsp.getContentType()); + } + + public void testExportInTransferFormat() throws Exception + { + NodeRef recordFolder1 = TestUtilities.getRecordFolder(searchService, "Reports", + "AIS Audit Records", "January AIS Audit Records"); + assertNotNull(recordFolder1); + + NodeRef recordFolder2 = TestUtilities.getRecordFolder(searchService, "Reports", + "Unit Manning Documents", "1st Quarter Unit Manning Documents"); + assertNotNull(recordFolder2); + + String exportUrl = "/api/rma/admin/export"; + + // define JSON POST body + JSONObject jsonPostData = new JSONObject(); + JSONArray nodeRefs = new JSONArray(); + nodeRefs.put(recordFolder1.toString()); + nodeRefs.put(recordFolder2.toString()); + jsonPostData.put("nodeRefs", nodeRefs); + jsonPostData.put("transferFormat", true); + String jsonPostString = jsonPostData.toString(); + + // make the export request + Response rsp = sendRequest(new PostRequest(exportUrl, jsonPostString, APPLICATION_JSON), 200); + assertEquals("application/zip", rsp.getContentType()); + } + + public void testTransfer() throws Exception + { + // Test 404 status for non existent node + String transferId = "yyy"; + String nonExistentNode = "workspace/SpacesStore/09ca1e02-1c87-4a53-97e7-xxxxxxxxxxxx"; + String nonExistentUrl = MessageFormat.format(GET_TRANSFER_URL_FORMAT, nonExistentNode, transferId); + Response rsp = sendRequest(new GetRequest(nonExistentUrl), 404); + + // Test 400 status for node that isn't a file plan + NodeRef series = TestUtilities.getRecordSeries(searchService, "Reports"); + assertNotNull(series); + String seriesNodeUrl = series.toString().replace("://", "/"); + String wrongNodeUrl = MessageFormat.format(GET_TRANSFER_URL_FORMAT, seriesNodeUrl, transferId); + rsp = sendRequest(new GetRequest(wrongNodeUrl), 400); + + // Test 404 status for file plan with no transfers + NodeRef rootNode = this.rmService.getFilePlan(series); + String rootNodeUrl = rootNode.toString().replace("://", "/"); + String transferUrl = MessageFormat.format(GET_TRANSFER_URL_FORMAT, rootNodeUrl, transferId); + rsp = sendRequest(new GetRequest(transferUrl), 404); + + // Get test in state where a transfer will be present + NodeRef recordCategory = TestUtilities.getRecordCategory(searchService, "Civilian Files", "Foreign Employee Award Files"); + assertNotNull(recordCategory); + + UserTransaction txn = transactionService.getUserTransaction(false); + txn.begin(); + + NodeRef newRecordFolder = this.nodeService.createNode(recordCategory, ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, QName.createValidLocalName("recordFolder")), + TYPE_RECORD_FOLDER).getChildRef(); + Map folderProps = new HashMap(1); + folderProps.put(PROP_IDENTIFIER, "2009-000000" + nodeService.getProperty(newRecordFolder, ContentModel.PROP_NODE_DBID)); + nodeService.addProperties(newRecordFolder, folderProps); + + txn.commit(); + txn = transactionService.getUserTransaction(false); + txn.begin(); + + // Create 2 documents + Map props1 = new HashMap(1); + props1.put(ContentModel.PROP_NAME, "record1"); + NodeRef recordOne = this.nodeService.createNode(newRecordFolder, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "record1"), + ContentModel.TYPE_CONTENT, props1).getChildRef(); + + // Set the content + ContentWriter writer1 = this.contentService.getWriter(recordOne, ContentModel.PROP_CONTENT, true); + writer1.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer1.setEncoding("UTF-8"); + writer1.putContent("There is some content for record 1"); + + Map props2 = new HashMap(1); + props2.put(ContentModel.PROP_NAME, "record2"); + NodeRef recordTwo = this.nodeService.createNode(newRecordFolder, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "record2"), + ContentModel.TYPE_CONTENT, props2).getChildRef(); + + // Set the content + ContentWriter writer2 = this.contentService.getWriter(recordTwo, ContentModel.PROP_CONTENT, true); + writer2.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer2.setEncoding("UTF-8"); + writer2.putContent("There is some content for record 2"); + txn.commit(); + + // declare the new record + txn = transactionService.getUserTransaction(false); + txn.begin(); + declareRecord(recordOne); + declareRecord(recordTwo); + + // prepare for the transfer + Map params = new HashMap(3); + params.put(CompleteEventAction.PARAM_EVENT_NAME, "case_complete"); + params.put(CompleteEventAction.PARAM_EVENT_COMPLETED_AT, new Date()); + params.put(CompleteEventAction.PARAM_EVENT_COMPLETED_BY, "gavinc"); + this.rmActionService.executeRecordsManagementAction(newRecordFolder, "completeEvent", params); + this.rmActionService.executeRecordsManagementAction(newRecordFolder, "cutoff"); + + DispositionAction da = dispositionService.getNextDispositionAction(newRecordFolder); + assertNotNull(da); + assertEquals("transfer", da.getName()); + txn.commit(); + + // Clock the asOf date back to ensure eligibility + txn = transactionService.getUserTransaction(false); + txn.begin(); + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.HOUR, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + this.nodeService.setProperty(da.getNodeRef(), PROP_DISPOSITION_AS_OF, calendar.getTime()); + + // Do the transfer + this.rmActionService.executeRecordsManagementAction(newRecordFolder, "transfer", null); + txn.commit(); + + // check that there is a transfer object present + List assocs = this.nodeService.getChildAssocs(rootNode, ASSOC_TRANSFERS, RegexQNamePattern.MATCH_ALL); + assertNotNull(assocs); + assertTrue(assocs.size() > 0); + + // Test 404 status for file plan with transfers but not the requested one + rootNode = this.rmService.getFilePlan(newRecordFolder); + rootNodeUrl = rootNode.toString().replace("://", "/"); + transferUrl = MessageFormat.format(GET_TRANSFER_URL_FORMAT, rootNodeUrl, transferId); + rsp = sendRequest(new GetRequest(transferUrl), 404); + + // retrieve the id of the last transfer + NodeRef transferNodeRef = assocs.get(assocs.size()-1).getChildRef(); + Date transferDate = (Date)nodeService.getProperty(transferNodeRef, ContentModel.PROP_CREATED); + + // Test successful retrieval of transfer archive + transferId = transferNodeRef.getId(); + transferUrl = MessageFormat.format(GET_TRANSFER_URL_FORMAT, rootNodeUrl, transferId); + rsp = sendRequest(new GetRequest(transferUrl), 200); + assertEquals("application/zip", rsp.getContentType()); + + // Test retrieval of transfer report, will be in JSON format + String transferReportUrl = MessageFormat.format(TRANSFER_REPORT_URL_FORMAT, rootNodeUrl, transferId); + rsp = sendRequest(new GetRequest(transferReportUrl), 200); + //System.out.println(rsp.getContentAsString()); + assertEquals("application/json", rsp.getContentType()); + JSONObject jsonRsp = new JSONObject(new JSONTokener(rsp.getContentAsString())); + assertTrue(jsonRsp.has("data")); + JSONObject data = jsonRsp.getJSONObject("data"); + assertTrue(data.has("transferDate")); + Date transferDateRsp = ISO8601DateFormat.parse(data.getString("transferDate")); + assertEquals(transferDate, transferDateRsp); + assertTrue(data.has("transferPerformedBy")); + assertEquals("System", data.getString("transferPerformedBy")); + assertTrue(data.has("dispositionAuthority")); + assertEquals("N1-218-00-3 item 18", data.getString("dispositionAuthority")); + assertTrue(data.has("items")); + JSONArray items = data.getJSONArray("items"); + assertEquals("Expecting 1 transferred folder", 1, items.length()); + JSONObject folder = items.getJSONObject(0); + assertTrue(folder.has("type")); + assertEquals("folder", folder.getString("type")); + assertTrue(folder.has("name")); + assertTrue(folder.getString("name").length() > 0); + assertTrue(folder.has("nodeRef")); + assertTrue(folder.getString("nodeRef").startsWith("workspace://SpacesStore/")); + assertTrue(folder.has("id")); + + // "id" should start with year-number pattern e.g. 2009-0000 + // This regular expression represents a string that starts with 4 digits followed by a hyphen, + // then 4 more digits and then any other characters + final String idRegExp = "^\\d{4}-\\d{4}.*"; + assertTrue(folder.getString("id").matches(idRegExp)); + + assertTrue(folder.has("children")); + JSONArray records = folder.getJSONArray("children"); + assertEquals("Expecting 2 transferred records", 2, records.length()); + JSONObject record1 = records.getJSONObject(0); + assertTrue(record1.has("type")); + assertEquals("record", record1.getString("type")); + assertTrue(record1.has("name")); + assertEquals("record1", record1.getString("name")); + assertTrue(record1.has("nodeRef")); + assertTrue(record1.getString("nodeRef").startsWith("workspace://SpacesStore/")); + assertTrue(record1.has("id")); + assertTrue(record1.getString("id").matches(idRegExp)); + assertTrue(record1.has("declaredBy")); + assertEquals("System", record1.getString("declaredBy")); + assertTrue(record1.has("declaredAt")); + + // Test filing a transfer report as a record + + // Attempt to store transfer report at non existent destination, make sure we get 404 + JSONObject jsonPostData = new JSONObject(); + jsonPostData.put("destination", "workspace://SpacesStore/09ca1e02-1c87-4a53-97e7-xxxxxxxxxxxx"); + String jsonPostString = jsonPostData.toString(); + rsp = sendRequest(new PostRequest(transferReportUrl, jsonPostString, APPLICATION_JSON), 404); + + // Attempt to store audit log at wrong type of destination, make sure we get 400 + NodeRef wrongCategory = TestUtilities.getRecordCategory(searchService, "Civilian Files", + "Foreign Employee Award Files"); + assertNotNull(wrongCategory); + jsonPostData = new JSONObject(); + jsonPostData.put("destination", wrongCategory.toString()); + jsonPostString = jsonPostData.toString(); + rsp = sendRequest(new PostRequest(transferReportUrl, jsonPostString, APPLICATION_JSON), 400); + + // get record folder to file into + NodeRef destination = TestUtilities.getRecordFolder(searchService, "Civilian Files", + "Foreign Employee Award Files", "Christian Bohr"); + assertNotNull(destination); + + // Store the full audit log as a record + jsonPostData = new JSONObject(); + jsonPostData.put("destination", destination); + jsonPostString = jsonPostData.toString(); + rsp = sendRequest(new PostRequest(transferReportUrl, jsonPostString, APPLICATION_JSON), 200); + + // check the response + System.out.println(rsp.getContentAsString()); + jsonRsp = new JSONObject(new JSONTokener(rsp.getContentAsString())); + assertTrue(jsonRsp.has("success")); + assertTrue(jsonRsp.getBoolean("success")); + assertTrue(jsonRsp.has("record")); + assertNotNull(jsonRsp.get("record")); + assertTrue(nodeService.exists(new NodeRef(jsonRsp.getString("record")))); + assertTrue(jsonRsp.has("recordName")); + assertNotNull(jsonRsp.get("recordName")); + assertTrue(jsonRsp.getString("recordName").startsWith("report_")); + } + + public void testAudit() throws Exception + { + // call the list service to get audit events + Response rsp = sendRequest(new GetRequest(GET_LIST_URL), 200); + //System.out.println("GET : " + rsp.getContentAsString()); + assertEquals("application/json;charset=UTF-8", rsp.getContentType()); + + // get response as JSON and check + JSONObject jsonParsedObject = new JSONObject(new JSONTokener(rsp.getContentAsString())); + assertNotNull(jsonParsedObject); + JSONObject data = jsonParsedObject.getJSONObject("data"); + JSONObject events = data.getJSONObject("auditEvents"); + JSONArray items = events.getJSONArray("items"); + assertEquals(this.rmAuditService.getAuditEvents().size(), items.length()); + assertTrue(items.length() > 0); + JSONObject item = items.getJSONObject(0); + assertTrue(item.length() == 2); + assertTrue(item.has("label")); + assertTrue(item.has("value")); + + // get the full RM audit log and check response + rsp = sendRequest(new GetRequest(RMA_AUDITLOG_URL), 200); + assertEquals("application/json", rsp.getContentType()); + JSONObject jsonRsp = new JSONObject(new JSONTokener(rsp.getContentAsString())); + + // get the full RM audit log as an HTML report and check response + rsp = sendRequest(new GetRequest(RMA_AUDITLOG_URL + "?format=html"), 200); + assertEquals("text/html", rsp.getContentType()); + + // export the full RM audit log and check response + rsp = sendRequest(new GetRequest(RMA_AUDITLOG_URL + "?export=true"), 200); + assertEquals("application/json", rsp.getContentType()); + jsonRsp = new JSONObject(new JSONTokener(rsp.getContentAsString())); + + // get category + NodeRef recordCategory = TestUtilities.getRecordCategory(searchService, "Civilian Files", "Foreign Employee Award Files"); + assertNotNull(recordCategory); + + // construct the URL + String nodeUrl = recordCategory.toString().replace("://", "/"); + String auditUrl = MessageFormat.format(GET_NODE_AUDITLOG_URL_FORMAT, nodeUrl); + + // send request + rsp = sendRequest(new GetRequest(auditUrl), 200); + // check response + assertEquals("application/json", rsp.getContentType()); + jsonRsp = new JSONObject(new JSONTokener(rsp.getContentAsString())); + + // get the audit log with all restrictions in place + String filteredAuditUrl = auditUrl + "?user=gavinc&size=5&from=2009-01-01&to=2009-12-31&event=Login"; + rsp = sendRequest(new GetRequest(filteredAuditUrl), 200); + // check response + assertEquals("application/json", rsp.getContentType()); + jsonRsp = new JSONObject(new JSONTokener(rsp.getContentAsString())); + + // attempt to get the audit log with invalid restrictions in place + filteredAuditUrl = auditUrl + "?user=fred&size=abc&from=2009&to=2010&property=wrong"; + rsp = sendRequest(new GetRequest(filteredAuditUrl), 200); + assertEquals("application/json", rsp.getContentType()); + jsonRsp = new JSONObject(new JSONTokener(rsp.getContentAsString())); + + checkAuditStatus(true); + + // start the RM audit log + JSONObject jsonPostData = new JSONObject(); + jsonPostData.put("enabled", true); + String jsonPostString = jsonPostData.toString(); + rsp = sendRequest(new PutRequest(RMA_AUDITLOG_URL, jsonPostString, APPLICATION_JSON), 200); + + checkAuditStatus(true); + + // check the response + //System.out.println(rsp.getContentAsString()); + jsonRsp = new JSONObject(new JSONTokener(rsp.getContentAsString())); + JSONObject dataObj = (JSONObject)jsonRsp.get("data"); + assertNotNull("JSON 'data' object was null", dataObj); + assertTrue(dataObj.getBoolean("enabled")); + assertTrue(dataObj.has("started")); + assertTrue(dataObj.has("stopped")); + + // stop the RM audit log + jsonPostData = new JSONObject(); + jsonPostData.put("enabled", false); + jsonPostString = jsonPostData.toString(); + rsp = sendRequest(new PutRequest(RMA_AUDITLOG_URL, jsonPostString, APPLICATION_JSON), 200); + + checkAuditStatus(false); + + // check the response + //System.out.println(rsp.getContentAsString()); + jsonRsp = new JSONObject(new JSONTokener(rsp.getContentAsString())); + dataObj = (JSONObject)jsonRsp.get("data"); + assertNotNull("JSON 'data' object was null", dataObj); + assertFalse(dataObj.getBoolean("enabled")); + + // clear the RM audit log + rsp = sendRequest(new DeleteRequest(RMA_AUDITLOG_URL), 200); + //System.out.println(rsp.getContentAsString()); + jsonRsp = new JSONObject(new JSONTokener(rsp.getContentAsString())); + dataObj = (JSONObject)jsonRsp.get("data"); + assertNotNull("JSON 'data' object was null", dataObj); + assertFalse(dataObj.getBoolean("enabled")); + } + + private void checkAuditStatus(boolean expected) throws Exception + { + Response rsp = sendRequest(new GetRequest(RMA_AUDITLOG_STATUS_URL), 200); + JSONObject rspObj = new JSONObject(rsp.getContentAsString()); + JSONObject data = rspObj.getJSONObject("data"); + boolean enabled = data.getBoolean("enabled"); + assertEquals("Audit log status does not match expected status.", expected, enabled); + + } + + public void testFileAuditLogAsRecord() throws Exception + { + // Attempt to store audit log at non existent destination, make sure we get 404 + JSONObject jsonPostData = new JSONObject(); + jsonPostData.put("destination", "workspace://SpacesStore/09ca1e02-1c87-4a53-97e7-xxxxxxxxxxxx"); + String jsonPostString = jsonPostData.toString(); + Response rsp = sendRequest(new PostRequest(RMA_AUDITLOG_URL, jsonPostString, APPLICATION_JSON), 404); + + // Attempt to store audit log at wrong type of destination, make sure we get 400 + NodeRef recordCategory = TestUtilities.getRecordCategory(searchService, "Civilian Files", + "Foreign Employee Award Files"); + assertNotNull(recordCategory); + jsonPostData = new JSONObject(); + jsonPostData.put("destination", recordCategory.toString()); + jsonPostString = jsonPostData.toString(); + rsp = sendRequest(new PostRequest(RMA_AUDITLOG_URL, jsonPostString, APPLICATION_JSON), 400); + + // get record folder to file into + NodeRef destination = TestUtilities.getRecordFolder(searchService, "Civilian Files", + "Foreign Employee Award Files", "Christian Bohr"); + assertNotNull(destination); + + // Store the full audit log as a record + jsonPostData = new JSONObject(); + jsonPostData.put("destination", destination); + jsonPostString = jsonPostData.toString(); + rsp = sendRequest(new PostRequest(RMA_AUDITLOG_URL, jsonPostString, APPLICATION_JSON), 200); + + // check the response + System.out.println(rsp.getContentAsString()); + JSONObject jsonRsp = new JSONObject(new JSONTokener(rsp.getContentAsString())); + assertTrue(jsonRsp.has("success")); + assertTrue(jsonRsp.getBoolean("success")); + assertTrue(jsonRsp.has("record")); + assertNotNull(jsonRsp.get("record")); + assertTrue(nodeService.exists(new NodeRef(jsonRsp.getString("record")))); + assertTrue(jsonRsp.has("recordName")); + assertNotNull(jsonRsp.get("recordName")); + assertTrue(jsonRsp.getString("recordName").startsWith("audit_")); + + // Store a filtered audit log as a record + jsonPostData = new JSONObject(); + jsonPostData.put("destination", destination); + jsonPostData.put("size", "50"); + jsonPostData.put("user", "gavinc"); + jsonPostData.put("event", "Update Metadata"); + jsonPostData.put("property", "{http://www.alfresco.org/model/content/1.0}modified"); + jsonPostString = jsonPostData.toString(); + rsp = sendRequest(new PostRequest(RMA_AUDITLOG_URL, jsonPostString, APPLICATION_JSON), 200); + + // check the response + System.out.println(rsp.getContentAsString()); + jsonRsp = new JSONObject(new JSONTokener(rsp.getContentAsString())); + assertTrue(jsonRsp.has("success")); + assertTrue(jsonRsp.getBoolean("success")); + assertTrue(jsonRsp.has("record")); + assertNotNull(jsonRsp.get("record")); + assertTrue(nodeService.exists(new NodeRef(jsonRsp.getString("record")))); + assertTrue(jsonRsp.has("recordName")); + assertNotNull(jsonRsp.get("recordName")); + assertTrue(jsonRsp.getString("recordName").startsWith("audit_")); + } + + private void declareRecord(NodeRef recordOne) + { + // Declare record + Map propValues = this.nodeService.getProperties(recordOne); + propValues.put(RecordsManagementModel.PROP_PUBLICATION_DATE, new Date()); + /*List smList = new ArrayList(2); + smList.add("FOUO"); + smList.add("NOFORN"); + propValues.put(RecordsManagementModel.PROP_SUPPLEMENTAL_MARKING_LIST, (Serializable)smList);*/ + propValues.put(RecordsManagementModel.PROP_MEDIA_TYPE, "mediaTypeValue"); + propValues.put(RecordsManagementModel.PROP_FORMAT, "formatValue"); + propValues.put(RecordsManagementModel.PROP_DATE_RECEIVED, new Date()); + propValues.put(RecordsManagementModel.PROP_ORIGINATOR, "origValue"); + propValues.put(RecordsManagementModel.PROP_ORIGINATING_ORGANIZATION, "origOrgValue"); + propValues.put(ContentModel.PROP_TITLE, "titleValue"); + this.nodeService.setProperties(recordOne, propValues); + this.rmActionService.executeRecordsManagementAction(recordOne, "declareRecord"); + } + + private NodeRef retrievePreexistingRecordFolder() + { + final List resultNodeRefs = retrieveJanuaryAISVitalFolders(); + + return resultNodeRefs.get(0); + } + + private List retrieveJanuaryAISVitalFolders() + { + String typeQuery = "TYPE:\"" + TYPE_RECORD_FOLDER + "\" AND @cm\\:name:\"January AIS Audit Records\""; + ResultSet types = this.searchService.query(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, SearchService.LANGUAGE_LUCENE, typeQuery); + + final List resultNodeRefs = types.getNodeRefs(); + types.close(); + return resultNodeRefs; + } + + private NodeRef createRecord(NodeRef recordFolder, String name, String title) + { + // Create the document + Map props = new HashMap(1); + props.put(ContentModel.PROP_NAME, name); + props.put(ContentModel.PROP_TITLE, title); + NodeRef recordOne = this.nodeService.createNode(recordFolder, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, name), + ContentModel.TYPE_CONTENT, + props).getChildRef(); + + // Set the content + ContentWriter writer = this.contentService.getWriter(recordOne, ContentModel.PROP_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.setEncoding("UTF-8"); + writer.putContent("There is some content in this record"); + + return recordOne; + } + + public void testPropertyLabelWithAccentedChars() throws Exception + { + final long number = System.currentTimeMillis(); + + // Create a property with a simple name + final String simplePropId = "simpleId" + number; + postCustomPropertyDefinition("simple", simplePropId); + + // Create a property whose name has accented chars + final String originalAccentedLabel = "øoê≈çœ"; + final String accentedPropId = "accentedId" + number; + postCustomPropertyDefinition(originalAccentedLabel, accentedPropId); + + // We'll update the label on the simple-name property a few times. + // This will cause the repeated read and write of the entire RM custom model xml file + // This should also leave the accented-char property unchanged. + putCustomPropDefinition("one", simplePropId); + putCustomPropDefinition("two", simplePropId); + putCustomPropDefinition("three", simplePropId); + putCustomPropDefinition("four", simplePropId); + putCustomPropDefinition("five", simplePropId); + + // Now get all the custom properties back. + String rspContent = getCustomProperties(); + + JSONObject rspObject = new JSONObject(new JSONTokener(rspContent)); + JSONObject dataObj = rspObject.getJSONObject("data"); + assertNotNull("jsonObject was null", dataObj); + + JSONObject customPropertiesObj = dataObj.getJSONObject("customProperties"); + assertNotNull("customPropertiesObj was null", customPropertiesObj); + + JSONObject accentedPropertyObj = customPropertiesObj.getJSONObject(RecordsManagementCustomModel.RM_CUSTOM_PREFIX + + ":" + accentedPropId); + assertNotNull("accentedPropertyObj was null", accentedPropertyObj); + + String labelObj = accentedPropertyObj.getString("label"); + assertEquals("labelObj was changed.", originalAccentedLabel, labelObj); + } + + private void putCustomPropDefinition(String label, String id) throws JSONException, IOException, + UnsupportedEncodingException + { + String jsonString = new JSONStringer().object() + .key("label").value(label) + .endObject() + .toString(); + + String propDefnUrl = "/api/rma/admin/custompropertydefinitions/" + id; + Response rsp = sendRequest(new PutRequest(propDefnUrl, + jsonString, APPLICATION_JSON), 200); + + String rspContent = rsp.getContentAsString(); +// System.out.println(rspContent); + + JSONObject jsonRsp = new JSONObject(new JSONTokener(rspContent)); + String urlOfNewPropDef = jsonRsp.getString("url"); + assertNotNull("urlOfNewPropDef was null.", urlOfNewPropDef); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RoleRestApiTest.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RoleRestApiTest.java new file mode 100644 index 0000000000..a7d870fbce --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RoleRestApiTest.java @@ -0,0 +1,309 @@ +/* + * 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.module.org_alfresco_module_rm.test.webscript; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.capability.Capability; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.web.scripts.BaseWebScriptTest; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.util.GUID; +import org.json.JSONArray; +import org.json.JSONObject; +import org.springframework.extensions.webscripts.TestWebScriptServer.DeleteRequest; +import org.springframework.extensions.webscripts.TestWebScriptServer.GetRequest; +import org.springframework.extensions.webscripts.TestWebScriptServer.PostRequest; +import org.springframework.extensions.webscripts.TestWebScriptServer.PutRequest; +import org.springframework.extensions.webscripts.TestWebScriptServer.Response; + +/** + * This class tests the Rest API for disposition related operations + * + * @author Roy Wetherall + */ +public class RoleRestApiTest extends BaseWebScriptTest implements RecordsManagementModel +{ + protected static StoreRef SPACES_STORE = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "SpacesStore"); + protected static final String GET_ROLES_URL = "/api/rma/admin/rmroles"; + protected static final String SERVICE_URL_PREFIX = "/alfresco/service"; + protected static final String APPLICATION_JSON = "application/json"; + + protected NodeService nodeService; + protected RecordsManagementService rmService; + protected RecordsManagementSecurityService rmSecurityService; + + private NodeRef rmRootNode; + + @Override + protected void setUp() throws Exception + { + super.setUp(); + this.nodeService = (NodeService) getServer().getApplicationContext().getBean("NodeService"); + this.rmService = (RecordsManagementService)getServer().getApplicationContext().getBean("RecordsManagementService"); + this.rmSecurityService = (RecordsManagementSecurityService)getServer().getApplicationContext().getBean("RecordsManagementSecurityService"); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + + List roots = rmService.getFilePlans(); + if (roots.size() != 0) + { + rmRootNode = roots.get(0); + } + else + { + NodeRef root = this.nodeService.getRootNode(new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "SpacesStore")); + rmRootNode = this.nodeService.createNode(root, ContentModel.ASSOC_CHILDREN, ContentModel.ASSOC_CHILDREN, TYPE_FILE_PLAN).getChildRef(); + } + + } + + public void testGetRoles() throws Exception + { + String role1 = GUID.generate(); + String role2 = GUID.generate(); + + // Create a couple or roles by hand + rmSecurityService.createRole(rmRootNode, role1, "My Test Role", getListOfCapabilities(5)); + rmSecurityService.createRole(rmRootNode, role2, "My Test Role Too", getListOfCapabilities(5)); + + // Add the admin user to one of the roles + rmSecurityService.assignRoleToAuthority(rmRootNode, role1, "admin"); + + try + { + // Get the roles + Response rsp = sendRequest(new GetRequest(GET_ROLES_URL),200); + String rspContent = rsp.getContentAsString(); + + JSONObject obj = new JSONObject(rspContent); + JSONObject roles = obj.getJSONObject("data"); + assertNotNull(roles); + + JSONObject roleObj = roles.getJSONObject(role1); + assertNotNull(roleObj); + assertEquals(role1, roleObj.get("name")); + assertEquals("My Test Role", roleObj.get("displayLabel")); + JSONArray caps = roleObj.getJSONArray("capabilities"); + assertNotNull(caps); + assertEquals(5, caps.length()); + + roleObj = roles.getJSONObject(role2); + assertNotNull(roleObj); + assertEquals(role2, roleObj.get("name")); + assertEquals("My Test Role Too", roleObj.get("displayLabel")); + caps = roleObj.getJSONArray("capabilities"); + assertNotNull(caps); + assertEquals(5, caps.length()); + + // Get the roles for "admin" + rsp = sendRequest(new GetRequest(GET_ROLES_URL + "?user=admin"),200); + rspContent = rsp.getContentAsString(); + + obj = new JSONObject(rspContent); + roles = obj.getJSONObject("data"); + assertNotNull(roles); + + roleObj = roles.getJSONObject(role1); + assertNotNull(roleObj); + assertEquals(role1, roleObj.get("name")); + assertEquals("My Test Role", roleObj.get("displayLabel")); + caps = roleObj.getJSONArray("capabilities"); + assertNotNull(caps); + assertEquals(5, caps.length()); + + assertFalse(roles.has(role2)); + } + finally + { + // Clean up + rmSecurityService.deleteRole(rmRootNode, role1); + rmSecurityService.deleteRole(rmRootNode, role2); + } + + } + + public void testPostRoles() throws Exception + { + Set caps = getListOfCapabilities(5); + JSONArray arrCaps = new JSONArray(); + for (Capability cap : caps) + { + arrCaps.put(cap.getName()); + } + + String roleName = GUID.generate(); + + JSONObject obj = new JSONObject(); + obj.put("name", roleName); + obj.put("displayLabel", "Display Label"); + obj.put("capabilities", arrCaps); + + Response rsp = sendRequest(new PostRequest(GET_ROLES_URL, obj.toString(), APPLICATION_JSON),200); + try + { + String rspContent = rsp.getContentAsString(); + + JSONObject resultObj = new JSONObject(rspContent); + JSONObject roleObj = resultObj.getJSONObject("data"); + assertNotNull(roleObj); + + assertNotNull(roleObj); + assertEquals(roleName, roleObj.get("name")); + assertEquals("Display Label", roleObj.get("displayLabel")); + JSONArray resultCaps = roleObj.getJSONArray("capabilities"); + assertNotNull(resultCaps); + assertEquals(5, resultCaps.length()); + } + finally + { + rmSecurityService.deleteRole(rmRootNode, roleName); + } + + } + + public void testPutRole() throws Exception + { + String role1 = GUID.generate(); + rmSecurityService.createRole(rmRootNode, role1, "My Test Role", getListOfCapabilities(5)); + + try + { + Set caps = getListOfCapabilities(4,8); + JSONArray arrCaps = new JSONArray(); + for (Capability cap : caps) + { + System.out.println(cap.getName()); + arrCaps.put(cap.getName()); + } + + JSONObject obj = new JSONObject(); + obj.put("name", role1); + obj.put("displayLabel", "Changed"); + obj.put("capabilities", arrCaps); + + // Get the roles + Response rsp = sendRequest(new PutRequest(GET_ROLES_URL + "/" + role1, obj.toString(), APPLICATION_JSON),200); + String rspContent = rsp.getContentAsString(); + + JSONObject result = new JSONObject(rspContent); + JSONObject roleObj = result.getJSONObject("data"); + assertNotNull(roleObj); + + assertNotNull(roleObj); + assertEquals(role1, roleObj.get("name")); + assertEquals("Changed", roleObj.get("displayLabel")); + JSONArray bob = roleObj.getJSONArray("capabilities"); + assertNotNull(bob); + assertEquals(4, bob.length()); + + // Bad requests + sendRequest(new PutRequest(GET_ROLES_URL + "/cheese", obj.toString(), APPLICATION_JSON), 404); + } + finally + { + // Clean up + rmSecurityService.deleteRole(rmRootNode, role1); + } + + } + + public void testGetRole() throws Exception + { + String role1 = GUID.generate(); + rmSecurityService.createRole(rmRootNode, role1, "My Test Role", getListOfCapabilities(5)); + + try + { + // Get the roles + Response rsp = sendRequest(new GetRequest(GET_ROLES_URL + "/" + role1),200); + String rspContent = rsp.getContentAsString(); + + JSONObject obj = new JSONObject(rspContent); + JSONObject roleObj = obj.getJSONObject("data"); + assertNotNull(roleObj); + + assertNotNull(roleObj); + assertEquals(role1, roleObj.get("name")); + assertEquals("My Test Role", roleObj.get("displayLabel")); + JSONArray caps = roleObj.getJSONArray("capabilities"); + assertNotNull(caps); + assertEquals(5, caps.length()); + + // Bad requests + sendRequest(new GetRequest(GET_ROLES_URL + "/cheese"), 404); + } + finally + { + // Clean up + rmSecurityService.deleteRole(rmRootNode, role1); + } + + } + + public void testDeleteRole() throws Exception + { + String role1 = GUID.generate(); + assertFalse(rmSecurityService.existsRole(rmRootNode, role1)); + rmSecurityService.createRole(rmRootNode, role1, "My Test Role", getListOfCapabilities(5)); + assertTrue(rmSecurityService.existsRole(rmRootNode, role1)); + sendRequest(new DeleteRequest(GET_ROLES_URL + "/" + role1),200); + assertFalse(rmSecurityService.existsRole(rmRootNode, role1)); + + // Bad request + sendRequest(new DeleteRequest(GET_ROLES_URL + "/cheese"), 404); + } + + private Set getListOfCapabilities(int size) + { + return getListOfCapabilities(size, 0); + } + + private Set getListOfCapabilities(int size, int offset) + { + Set result = new HashSet(size); + Set caps = rmSecurityService.getCapabilities(); + int count = 0; + for (Capability cap : caps) + { + if (count < size+offset) + { + if (count >= offset) + { + result.add(cap); + } + } + else + { + break; + } + count ++; + } + return result; + } + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/vital/BroadcastVitalRecordDefinitionAction.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/vital/BroadcastVitalRecordDefinitionAction.java new file mode 100644 index 0000000000..e7adec62e7 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/vital/BroadcastVitalRecordDefinitionAction.java @@ -0,0 +1,136 @@ +/* + * 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.module.org_alfresco_module_rm.vital; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.action.RMActionExecuterAbstractBase; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.Period; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; + +/** + * Action to implement the consequences of a change to the value of the VitalRecordDefinition properties. When the + * VitalRecordIndicator or the reviewPeriod properties are changed on a record container, then any descendant folders or + * records must be updated as a consequence. Descendant folders should have their reviewPeriods and/or + * vitalRecordIndicators updated to match the new value. Descendant records should have their reviewAsOf date updated. + * + * @author Neil McErlean + */ +public class BroadcastVitalRecordDefinitionAction extends RMActionExecuterAbstractBase +{ + /** + * @see org.alfresco.repo.action.executer.ActionExecuterAbstractBase#executeImpl(org.alfresco.service.cmr.action.Action, + * org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + protected void executeImpl(Action action, NodeRef actionedUponNodeRef) + { + this.propagateChangeToChildrenOf(actionedUponNodeRef); + } + + /** + * @see org.alfresco.repo.action.ParameterizedItemAbstractBase#addParameterDefinitions(java.util.List) + */ + @Override + protected void addParameterDefinitions(List paramList) + { + // Intentionally empty + } + + private void propagateChangeToChildrenOf(NodeRef actionedUponNodeRef) + { + Map parentProps = nodeService.getProperties(actionedUponNodeRef); + boolean parentVri = (Boolean) parentProps.get(PROP_VITAL_RECORD_INDICATOR); + Period parentReviewPeriod = (Period) parentProps.get(PROP_REVIEW_PERIOD); + + List assocs = this.nodeService.getChildAssocs(actionedUponNodeRef, ContentModel.ASSOC_CONTAINS, RegexQNamePattern.MATCH_ALL); + for (ChildAssociationRef nextAssoc : assocs) + { + NodeRef nextChild = nextAssoc.getChildRef(); + + // If the child is a record, then the VitalRecord aspect needs to be applied or updated + if (recordsManagementService.isRecord(nextChild)) + { + if (parentVri) + { + VitalRecordDefinition vrDefn = vitalRecordService.getVitalRecordDefinition(nextChild); + Map aspectProps = new HashMap(); + aspectProps.put(PROP_REVIEW_AS_OF, vrDefn.getNextReviewDate()); + + nodeService.addAspect(nextChild, RecordsManagementModel.ASPECT_VITAL_RECORD, aspectProps); + } + else + { + nodeService.removeAspect(nextChild, RecordsManagementModel.ASPECT_VITAL_RECORD); + } + } + else + // copy the vitalRecordDefinition properties from the parent to the child + { + Map childProps = nodeService.getProperties(nextChild); + childProps.put(PROP_REVIEW_PERIOD, parentReviewPeriod); + childProps.put(PROP_VITAL_RECORD_INDICATOR, parentVri); + nodeService.setProperties(nextChild, childProps); + } + + // Recurse down the containment hierarchy to all containers + if (recordsManagementService.isRecord(nextChild) == false) + { + this.propagateChangeToChildrenOf(nextChild); + } + } + } + + @Override + public boolean isExecutableImpl(NodeRef filePlanComponent, Map parameters, boolean throwException) + { + return true; + } + + @Override + public Set getProtectedProperties() + { + HashSet qnames = new HashSet(); + qnames.add(PROP_REVIEW_PERIOD); + qnames.add(PROP_VITAL_RECORD_INDICATOR); + qnames.add(PROP_REVIEW_AS_OF); + return qnames; + } + + @Override + public Set getProtectedAspects() + { + HashSet qnames = new HashSet(); + qnames.add(RecordsManagementModel.ASPECT_VITAL_RECORD); + return qnames; + } + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/vital/ReviewedAction.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/vital/ReviewedAction.java new file mode 100644 index 0000000000..731d7b67fe --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/vital/ReviewedAction.java @@ -0,0 +1,121 @@ +/* + * 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.module.org_alfresco_module_rm.vital; + +import java.io.Serializable; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.module.org_alfresco_module_rm.action.RMActionExecuterAbstractBase; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Reviewed action. + * + * @author Neil McErlean + */ +public class ReviewedAction extends RMActionExecuterAbstractBase +{ + private static Log logger = LogFactory.getLog(ReviewedAction.class); + + /** + * + * @see org.alfresco.repo.action.executer.ActionExecuterAbstractBase#executeImpl(org.alfresco.service.cmr.action.Action, + * org.alfresco.service.cmr.repository.NodeRef) + */ + @Override + protected void executeImpl(Action action, NodeRef actionedUponNodeRef) + { + VitalRecordDefinition vrDef = vitalRecordService.getVitalRecordDefinition(actionedUponNodeRef); + if (vrDef != null && vrDef.isEnabled() == true) + { + if (recordsManagementService.isRecord(actionedUponNodeRef) == true) + { + reviewRecord(actionedUponNodeRef, vrDef); + } + else if (recordsManagementService.isRecordFolder(actionedUponNodeRef) == true) + { + for (NodeRef record : recordsManagementService.getRecords(actionedUponNodeRef)) + { + reviewRecord(record, vrDef); + } + } + } + } + + /** + * Make record as reviewed. + * + * @param nodeRef + * @param vrDef + */ + private void reviewRecord(NodeRef nodeRef, VitalRecordDefinition vrDef) + { + // Calculate the next review date + Date reviewAsOf = vrDef.getNextReviewDate(); + if (reviewAsOf != null) + { + // Log + if (logger.isDebugEnabled()) + { + StringBuilder msg = new StringBuilder(); + msg.append("Setting new reviewAsOf property [") + .append(reviewAsOf) + .append("] on ") + .append(nodeRef); + logger.debug(msg.toString()); + } + + this.nodeService.setProperty(nodeRef, PROP_REVIEW_AS_OF, reviewAsOf); + //TODO And record previous review date, time, user + } + } + + /** + * + * @see org.alfresco.repo.action.ParameterizedItemAbstractBase#addParameterDefinitions(java.util.List) + */ + @Override + protected void addParameterDefinitions(List paramList) + { + // Intentionally empty + } + + @Override + public Set getProtectedProperties() + { + HashSet qnames = new HashSet(); + qnames.add(PROP_REVIEW_AS_OF); + return qnames; + } + + @Override + protected boolean isExecutableImpl(NodeRef filePlanComponent, Map parameters, boolean throwException) + { + return true; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/vital/VitalRecordDefinition.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/vital/VitalRecordDefinition.java new file mode 100644 index 0000000000..e83c79d9a8 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/vital/VitalRecordDefinition.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2005-2012 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.module.org_alfresco_module_rm.vital; + +import java.util.Date; + +import org.alfresco.service.cmr.repository.Period; + +/** + * Vital record definition interface + * + * @author Roy Wetherall + */ +public interface VitalRecordDefinition +{ + /** + * Indicates whether the vital record definition is enabled or not. + *

+ * Note: a result of false indicates that the vital record definition is inactive + * therefore does not impose the rules associated with vital record review on + * associated nodes. + * + * @return boolean true if enabled, false otherwise + */ + boolean isEnabled(); + + /** + * Review period for vital records + * + * @return Period review period + */ + Period getReviewPeriod(); + + /** + * Gets the next review date based on the review period + * + * @return Date date of the next review + */ + Date getNextReviewDate(); +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/vital/VitalRecordDefinitionImpl.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/vital/VitalRecordDefinitionImpl.java new file mode 100644 index 0000000000..571992b247 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/vital/VitalRecordDefinitionImpl.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2005-2012 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.module.org_alfresco_module_rm.vital; + +import java.util.Date; + +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.Period; + +/** + * Vital record definition implementation class + * + * @author Roy Wetherall + */ +public class VitalRecordDefinitionImpl implements VitalRecordDefinition, RecordsManagementModel +{ + /** Indicates whether the vital record definition is enabled or not */ + private boolean enabled = false; + + /** Vital record review period */ + private Period reviewPeriod = new Period("none|0"); + + /** + * Constructor. + * + * @param enabled + * @param reviewPeriod + */ + /* package */ VitalRecordDefinitionImpl(boolean enabled, Period reviewPeriod) + { + this.enabled = enabled; + if (reviewPeriod != null) + { + this.reviewPeriod = reviewPeriod; + } + } + + /** + * Helper method to create vital record definition from node reference. + * + * @param nodeService + * @param nodeRef + * @return + */ + /* package */ static VitalRecordDefinition create(NodeService nodeService, NodeRef nodeRef) + { + Boolean enabled = (Boolean)nodeService.getProperty(nodeRef, PROP_VITAL_RECORD_INDICATOR); + Period reviewPeriod = (Period)nodeService.getProperty(nodeRef, PROP_REVIEW_PERIOD); + return new VitalRecordDefinitionImpl(enabled, reviewPeriod); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.vital.VitalRecordDefinition#isEnabled() + */ + @Override + public boolean isEnabled() + { + return enabled; + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.vital.VitalRecordDefinition#getNextReviewDate() + */ + public Date getNextReviewDate() + { + return getReviewPeriod().getNextDate(new Date()); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.vital.VitalRecordDefinition#getReviewPeriod() + */ + public Period getReviewPeriod() + { + return reviewPeriod; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/vital/VitalRecordService.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/vital/VitalRecordService.java new file mode 100644 index 0000000000..248fd32c77 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/vital/VitalRecordService.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2005-2012 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.module.org_alfresco_module_rm.vital; + +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.Period; + +/** + * Vital Record Service. + * + * @author Roy Wetherall + * @since 2.0 + */ +public interface VitalRecordService +{ + /** + * Gets the vital record definition details for the node. + * + * @param nodeRef node reference + * @return VitalRecordDefinition vital record definition details + */ + VitalRecordDefinition getVitalRecordDefinition(NodeRef nodeRef); + + /** + * Sets the vital record definition values for a given node. + * + * @param nodeRef + * @param enabled + * @param reviewPeriod + * @return + */ + VitalRecordDefinition setVitalRecordDefintion(NodeRef nodeRef, boolean enabled, Period reviewPeriod); + + /** + * Indicates whether the record is a vital one or not. + * + * @param nodeRef node reference + * @return boolean true if this is a vital record, false otherwise + */ + boolean isVitalRecord(NodeRef nodeRef); + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/vital/VitalRecordServiceImpl.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/vital/VitalRecordServiceImpl.java new file mode 100644 index 0000000000..7ab4305c76 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/vital/VitalRecordServiceImpl.java @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2005-2012 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.module.org_alfresco_module_rm.vital; + +import java.io.Serializable; +import java.util.Map; + +import org.alfresco.module.org_alfresco_module_rm.FilePlanComponentKind; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.repo.node.NodeServicePolicies; +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.policy.Behaviour.NotificationFrequency; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.Period; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.ParameterCheck; + +/** + * Vital record service interface implementation. + * + * @author Roy Wetherall + * @since 2.0 + */ +public class VitalRecordServiceImpl implements VitalRecordService, + RecordsManagementModel, + NodeServicePolicies.OnUpdatePropertiesPolicy, + NodeServicePolicies.OnAddAspectPolicy +{ + /** Services */ + private NodeService nodeService; + private PolicyComponent policyComponent; + private RecordsManagementService rmService; + private RecordsManagementActionService rmActionService; + + /** Behaviours */ + private JavaBehaviour onUpdateProperties; + private JavaBehaviour onAddAspect; + + /** + * @param nodeService node service + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * @param policyComponent policy component + */ + public void setPolicyComponent(PolicyComponent policyComponent) + { + this.policyComponent = policyComponent; + } + + /** + * @param rmService records management service + */ + public void setRecordsManagementService(RecordsManagementService rmService) + { + this.rmService = rmService; + } + + /** + * @param rmActionService records management action service + */ + public void setRecordsManagementActionService(RecordsManagementActionService rmActionService) + { + this.rmActionService = rmActionService; + } + + /** + * Init method. + */ + public void init() + { + onUpdateProperties = new JavaBehaviour(this, "onUpdateProperties", NotificationFrequency.TRANSACTION_COMMIT); + policyComponent.bindClassBehaviour( + NodeServicePolicies.OnUpdatePropertiesPolicy.QNAME, + ASPECT_VITAL_RECORD_DEFINITION, + onUpdateProperties); + + onAddAspect = new JavaBehaviour(this, "onAddAspect", NotificationFrequency.TRANSACTION_COMMIT); + policyComponent.bindClassBehaviour( + NodeServicePolicies.OnAddAspectPolicy.QNAME, + ASPECT_VITAL_RECORD_DEFINITION, + onAddAspect); + } + + /** + * @see org.alfresco.repo.node.NodeServicePolicies.OnUpdatePropertiesPolicy#onUpdateProperties(org.alfresco.service.cmr.repository.NodeRef, java.util.Map, java.util.Map) + */ + @Override + public void onUpdateProperties(NodeRef nodeRef, Map before, Map after) + { + if (nodeService.exists(nodeRef) == true) + { + rmActionService.executeRecordsManagementAction(nodeRef, "broadcastVitalRecordDefinition"); + } + } + + /** + * @see org.alfresco.repo.node.NodeServicePolicies.OnAddAspectPolicy#onAddAspect(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName) + */ + @Override + public void onAddAspect(NodeRef nodeRef, QName aspectTypeQName) + { + ParameterCheck.mandatory("nodeRef", nodeRef); + ParameterCheck.mandatory("aspectTypeQName", aspectTypeQName); + + if (nodeService.exists(nodeRef) == true) + { + onUpdateProperties.disable(); + try + { + // get the immediate parent + NodeRef parentRef = nodeService.getPrimaryParent(nodeRef).getParentRef(); + + // is the parent a record category + if (parentRef != null && + FilePlanComponentKind.RECORD_CATEGORY.equals(rmService.getFilePlanComponentKind(parentRef)) == true) + { + // is the child a record category or folder + FilePlanComponentKind kind = rmService.getFilePlanComponentKind(nodeRef); + if (kind.equals(FilePlanComponentKind.RECORD_CATEGORY) == true || + kind.equals(FilePlanComponentKind.RECORD_FOLDER) == true) + { + // set the vital record definition values to match that of the parent + nodeService.setProperty(nodeRef, + PROP_VITAL_RECORD_INDICATOR, + nodeService.getProperty(parentRef, PROP_VITAL_RECORD_INDICATOR)); + nodeService.setProperty(nodeRef, + PROP_REVIEW_PERIOD, + nodeService.getProperty(parentRef, PROP_REVIEW_PERIOD)); + } + } + } + finally + { + onUpdateProperties.enable(); + } + } + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementService#getVitalRecordDefinition(org.alfresco.service.cmr.repository.NodeRef) + */ + public VitalRecordDefinition getVitalRecordDefinition(NodeRef nodeRef) + { + VitalRecordDefinition result = null; + + FilePlanComponentKind kind = rmService.getFilePlanComponentKind(nodeRef); + if (FilePlanComponentKind.RECORD.equals(kind) == true) + { + result = resolveVitalRecordDefinition(nodeRef); + } + else + { + if (nodeService.hasAspect(nodeRef, ASPECT_VITAL_RECORD_DEFINITION) == true) + { + result = VitalRecordDefinitionImpl.create(nodeService, nodeRef); + } + } + + return result; + } + + /** + * Resolves the record vital definition. + *

+ * NOTE: Currently we only support the resolution of the vital record definition from the + * primary record parent. ie the record folder the record was originally filed within. + *

+ * TODO: Add an algorithm to resolve the correct vital record definition when a record is filed in many + * record folders. + * + * @param record + * @return VitalRecordDefinition + */ + private VitalRecordDefinition resolveVitalRecordDefinition(NodeRef record) + { + NodeRef parent = nodeService.getPrimaryParent(record).getParentRef(); + return getVitalRecordDefinition(parent); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.vital.VitalRecordService#setVitalRecordDefintion(org.alfresco.service.cmr.repository.NodeRef, boolean, org.alfresco.service.cmr.repository.Period) + */ + @Override + public VitalRecordDefinition setVitalRecordDefintion(NodeRef nodeRef, boolean enabled, Period reviewPeriod) + { + // Check params + ParameterCheck.mandatory("nodeRef", nodeRef); + ParameterCheck.mandatory("enabled", enabled); + + // Set the properties (will automatically add the vital record definition aspect) + nodeService.setProperty(nodeRef, PROP_VITAL_RECORD_INDICATOR, enabled); + nodeService.setProperty(nodeRef, PROP_REVIEW_PERIOD, reviewPeriod); + + return new VitalRecordDefinitionImpl(enabled, reviewPeriod); + } + + /** + * @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementService#isVitalRecord(org.alfresco.service.cmr.repository.NodeRef) + */ + public boolean isVitalRecord(NodeRef nodeRef) + { + return nodeService.hasAspect(nodeRef, ASPECT_VITAL_RECORD); + } +} diff --git a/rm-server/test-resources/testCaveatConfig1.json b/rm-server/test-resources/testCaveatConfig1.json new file mode 100644 index 0000000000..1fac03e309 --- /dev/null +++ b/rm-server/test-resources/testCaveatConfig1.json @@ -0,0 +1,8 @@ +{ + "rmc:prjList": { + "admin" : ["Project A", "Project B", "Project C"] + }, + "rmc:smList": { + "admin" : ["NOFORN", "NOCONTRACT", "FOUO", "FGI"] + } +} diff --git a/rm-server/test-resources/testCaveatConfig2.json b/rm-server/test-resources/testCaveatConfig2.json new file mode 100644 index 0000000000..fec003535f --- /dev/null +++ b/rm-server/test-resources/testCaveatConfig2.json @@ -0,0 +1,27 @@ +{ + "rmc:prjList": { + "dmartinz" : ["Project A", "Project B", "Project C"], + "jrogers" : ["Project A", "Project B"], + "GROUP_Engineering" : ["Project A"], + "dfranco" : ["Project A", "Project C"], + "admin" : ["Project A", "Project B", "Project C"], + "GROUP_Finance" : ["Project C"] + }, + "rmc:smList": { + "jrangel" : ["NOFORN", "NOCONTRACT", "FOUO", "FGI"], + "dmartinz" : ["NOFORN", "NOCONTRACT", "FOUO", "FGI"], + "jrogers" : ["NOFORN", "NOCONTRACT", "FOUO", "FGI"], + "hmcneil" : ["NOFORN", "NOCONTRACT", "FOUO", "FGI"], + "dfranco" : ["NOFORN", "FOUO"], + "gsmith" : ["NOFORN", "FOUO"], + "eharris" : ["NOCONTRACT", "FOUO"], + "bbayless" : ["NOCONTRACT", "FOUO"], + "mhouse" : ["NOCONTRACT", "FOUO"], + "aly" : ["FOUO"], + "dsandy" : ["FGI"], + "driggs" : ["NOFORN"], + "admin" : ["FOUO", "NOFORN"], + "test1" : ["NOCONTRACT"], + "GROUP_test1" : ["NOCONTRACT"] + } +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000000..36e36d73b6 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include 'rm-server', 'rm-share' \ No newline at end of file From 96421f1f64e1e36f7de4cdcb8dbf2d7414f151b9 Mon Sep 17 00:00:00 2001 From: Roy Wetherall Date: Thu, 22 Mar 2012 02:57:42 +0000 Subject: [PATCH 04/24] RM: Adding dependancy folders and updated build properties git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/modules/recordsmanagement/HEAD@34678 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- build.gradle | 10 +++++----- gradle.properties | 3 ++- rm-server/gradle.properties | 2 +- .../libs/test/spring-webscripts-1.0.0-tests.jar | Bin 0 -> 76498 bytes 4 files changed, 8 insertions(+), 7 deletions(-) create mode 100644 rm-server/libs/test/spring-webscripts-1.0.0-tests.jar diff --git a/build.gradle b/build.gradle index ec61a39005..27385ac861 100644 --- a/build.gradle +++ b/build.gradle @@ -5,21 +5,21 @@ subprojects { sourceSets { main { java { - srcDir 'source/java' + srcDir sourceJavaDir } } } repositories { flatDir { - dirs 'libs' + dirs libsDir } mavenCentral() } dependencies { - compile fileTree(dir: 'libs', include: '*.jar') + compile fileTree(dir: libsDir, include: '*.jar') compile fileTree(dir: 'libs/test', include: '*.jar') compile group: 'javax.servlet', name: 'servlet-api', version: '2.5' @@ -32,11 +32,11 @@ subprojects { // Clean out any existing jars ant.delete { - ant.fileset(dir: 'libs', includes: '*.jar') + ant.fileset(dir: libsDir, includes: '*.jar') } // Unpack WAR - ant.unzip(src: warFile, dest: 'libs') { + ant.unzip(src: warFile, dest: libsDir) { ant.patternset { ant.include(name: '**/*.jar') } diff --git a/gradle.properties b/gradle.properties index 8b13789179..29a87f859d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1,2 @@ - +sourceJavaDir=source/java +libsDir=libs diff --git a/rm-server/gradle.properties b/rm-server/gradle.properties index 16e6a91c08..1014ee0ed2 100644 --- a/rm-server/gradle.properties +++ b/rm-server/gradle.properties @@ -1,2 +1,2 @@ -warFile=deps/alfresco.war +warFile=build/war/alfresco.war warFileName=Alfresco diff --git a/rm-server/libs/test/spring-webscripts-1.0.0-tests.jar b/rm-server/libs/test/spring-webscripts-1.0.0-tests.jar new file mode 100644 index 0000000000000000000000000000000000000000..1b386b0743f0f0ba53db717d0d9559f2f7c749e1 GIT binary patch literal 76498 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 Date: Thu, 22 Mar 2012 03:16:17 +0000 Subject: [PATCH 05/24] RM: Added various ignores so that build artififacts and dependancies are not shown in SVN modified lists git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/modules/recordsmanagement/HEAD@34679 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 From 0edede13d1181528aca00f3b199de7c4f58c405f Mon Sep 17 00:00:00 2001 From: Roy Wetherall Date: Thu, 22 Mar 2012 10:29:57 +0000 Subject: [PATCH 06/24] RM: Build script updates git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/modules/recordsmanagement/HEAD@34686 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- build.gradle | 79 ++++++++++++++++++++++++++----------- rm-server/build.gradle | 11 ++++++ rm-server/gradle.properties | 2 +- 3 files changed, 68 insertions(+), 24 deletions(-) create mode 100644 rm-server/build.gradle diff --git a/build.gradle b/build.gradle index 27385ac861..049c72fc88 100644 --- a/build.gradle +++ b/build.gradle @@ -14,33 +14,66 @@ subprojects { flatDir { dirs libsDir } - mavenCentral() } - dependencies { - - compile fileTree(dir: libsDir, include: '*.jar') - - compile fileTree(dir: 'libs/test', include: '*.jar') - compile group: 'javax.servlet', name: 'servlet-api', version: '2.5' - compile 'org.springframework:spring-test:2.5' + dependencies { + compile fileTree(dir: libsDir, include: '*.jar') } - task unpackWar << { - - println 'Unpacking ' + warFileName + ' WAR ...' - - // Clean out any existing jars - ant.delete { - ant.fileset(dir: libsDir, includes: '*.jar') - } - - // Unpack WAR - ant.unzip(src: warFile, dest: libsDir) { - ant.patternset { - ant.include(name: '**/*.jar') - } - ant.mapper(type: 'flatten') + compileJava.doFirst { + if (areLibsUnpacked(file(libsDir)) == false) { + tasks.unpackLibs.execute() } } + + task cleanLibs << { + ant.delete { + ant.fileset(dir: libsDir, includes: '*.jar') + } + } + + task refreshLibs (dependsOn: ['cleanLibs', 'unpackLibs']) + + task unpackLibs << { + + if (file(warFile).exists()) { + + println 'Unpacking libs from ' + warFileName + ' WAR' + + // unpack the jars from the war file + ant.unzip(src: warFile, dest: libsDir) { + ant.patternset { + ant.include(name: '**/*.jar') + } + ant.mapper(type: 'flatten') + } + } + else { + + // TODO eventually we will be able to retrieve the war file if we don't have it + println 'The ' + warFile + ' was not found.' + } + } + +// task amp << { +// +// ant.zip(destfile: 'build/dist/blar.amp', update: 'true') { +// +// ant.zipfileset(file: '../module.properties') +// ant.zipfileset(file: 'build/libs/rm.jar', prefix: 'lib') +// ant.zipfileset(dir: '/config', prefix: 'config') { +// ant.exclude(name: '**/module.properties') +// } +// } +// } + +} + +Boolean areLibsUnpacked(dir){ + + result = false; + dir.eachFileMatch(~/.*\.jar/) { + result = true; + } + return result; } diff --git a/rm-server/build.gradle b/rm-server/build.gradle new file mode 100644 index 0000000000..ab05b52b03 --- /dev/null +++ b/rm-server/build.gradle @@ -0,0 +1,11 @@ + +repositories { + mavenCentral() +} + +dependencies { + compile fileTree(dir: 'libs/test', include: '*.jar') + compile group: 'javax.servlet', name: 'servlet-api', version: '2.5' + compile 'org.springframework:spring-test:2.5' +} + diff --git a/rm-server/gradle.properties b/rm-server/gradle.properties index 1014ee0ed2..97dce3567d 100644 --- a/rm-server/gradle.properties +++ b/rm-server/gradle.properties @@ -1,2 +1,2 @@ -warFile=build/war/alfresco.war +warFile=war/alfresco.war warFileName=Alfresco From 96075afea4a1ea98cafa446b9c8c3b5166c5ad38 Mon Sep 17 00:00:00 2001 From: Roy Wetherall Date: Fri, 23 Mar 2012 05:16:03 +0000 Subject: [PATCH 07/24] RM: Latest updates to build scripts and Eclipse projects git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/modules/recordsmanagement/HEAD@34705 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- build.gradle | 83 ++--- rm-server/.classpath | 291 +++++++++++++++++- rm-server/.project | 21 +- .../.settings/org.eclipse.jdt.core.prefs | 12 + .../test/spring-webscripts-1.0.0-tests.jar | Bin 76498 -> 0 bytes .../capability/RMEntryVoter.java | 12 +- 6 files changed, 355 insertions(+), 64 deletions(-) create mode 100644 rm-server/.settings/org.eclipse.jdt.core.prefs delete mode 100644 rm-server/libs/test/spring-webscripts-1.0.0-tests.jar diff --git a/build.gradle b/build.gradle index 049c72fc88..524fb2f2fc 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,7 @@ subprojects { apply plugin: 'java' + apply plugin: 'eclipse' sourceSets { main { @@ -16,64 +17,74 @@ subprojects { } } - dependencies { - compile fileTree(dir: libsDir, include: '*.jar') + dependencies { + compile fileTree(dir: 'war/WEB-INF/lib', include: '*.jar') } compileJava.doFirst { - if (areLibsUnpacked(file(libsDir)) == false) { - tasks.unpackLibs.execute() + if (file('war/WEB-INF').exists() == false) { + tasks.expandWar.execute() } } - task cleanLibs << { + task cleanWar << { ant.delete { - ant.fileset(dir: libsDir, includes: '*.jar') + ant.fileset(dir: 'war', excludes: '*.war') } } - task refreshLibs (dependsOn: ['cleanLibs', 'unpackLibs']) + task refreshWar (dependsOn: ['cleanWar', 'expandWar']) - task unpackLibs << { + task expandWar << { if (file(warFile).exists()) { - println 'Unpacking libs from ' + warFileName + ' WAR' - - // unpack the jars from the war file - ant.unzip(src: warFile, dest: libsDir) { - ant.patternset { - ant.include(name: '**/*.jar') - } - ant.mapper(type: 'flatten') - } + println 'Expanding ' + warFileName + ' WAR' + ant.unzip(src: warFile, dest: 'war') } else { - // TODO eventually we will be able to retrieve the war file if we don't have it println 'The ' + warFile + ' was not found.' } } -// task amp << { -// -// ant.zip(destfile: 'build/dist/blar.amp', update: 'true') { -// -// ant.zipfileset(file: '../module.properties') -// ant.zipfileset(file: 'build/libs/rm.jar', prefix: 'lib') -// ant.zipfileset(dir: '/config', prefix: 'config') { -// ant.exclude(name: '**/module.properties') -// } -// } -// } + task amp << {} + assemble.doLast { + tasks.amp.execute() + } } + + +void assembleAmp(amp, module, jar, config, web){ -Boolean areLibsUnpacked(dir){ - - result = false; - dir.eachFileMatch(~/.*\.jar/) { - result = true; - } - return result; + ant.zip(destfile: amp, update: 'true') { + + def moduleProperties = module + '/module.properties' + def fileMapping = module + '/file-mapping.properties' + + if (file(moduleProperties).exists() == true) { + ant.zipfileset(file: moduleProperties) + } + + if (file(fileMapping).exists() == true) { + ant.zipfileset(file: fileMapping) + } + + if (jar != null) { + ant.zipfileset(file: jar, prefix: 'lib') + } + + if (config != null) { + ant.zipfileset(dir: config, prefix: 'config') { + ant.exclude(name: '**/module.properties') + ant.exclude(name: '**/file-mapping.properties') + } + } + + if (web != null) { + ant.zipfileset(file: web, prefix: 'web') + } + } } + diff --git a/rm-server/.classpath b/rm-server/.classpath index e191c98be8..54f363bd54 100644 --- a/rm-server/.classpath +++ b/rm-server/.classpath @@ -1,17 +1,282 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/rm-server/.project b/rm-server/.project index ec8bf86f5d..47a2d689a4 100644 --- a/rm-server/.project +++ b/rm-server/.project @@ -1,17 +1,16 @@ - Records Management - - - - - - org.eclipse.jdt.core.javabuilder - - - - + rm-server + + org.eclipse.jdt.core.javanature + + + org.eclipse.jdt.core.javabuilder + + + + diff --git a/rm-server/.settings/org.eclipse.jdt.core.prefs b/rm-server/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000000..1d946df44c --- /dev/null +++ b/rm-server/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,12 @@ +#Fri Mar 23 15:41:37 EST 2012 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.6 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.6 diff --git a/rm-server/libs/test/spring-webscripts-1.0.0-tests.jar b/rm-server/libs/test/spring-webscripts-1.0.0-tests.jar deleted file mode 100644 index 1b386b0743f0f0ba53db717d0d9559f2f7c749e1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 76498 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 Date: Fri, 23 Mar 2012 07:54:45 +0000 Subject: [PATCH 08/24] RM: Build scripts * war expanded in entirety and dependancies used as needed * Eclipse projects now building * Eclipse runtime helthier since have access to expanded WAR 'config' files on projects classpath * Custom Groovy task class added to construct AMP's * AMPs for both server and client build as part of the standard java 'assemble' task (called during 'build') * TODO check AMPs deploy * TODO extract properties * TODO construct correct Jar and Amp names based on version, etc git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/modules/recordsmanagement/HEAD@34706 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- build.gradle | 83 +++++++++++-------- rm-server/.classpath | 1 - rm-server/build.gradle | 12 ++- .../capability/RMEntryVoter.java | 51 ++++++++---- .../capability/impl/AbstractCapability.java | 3 +- 5 files changed, 93 insertions(+), 57 deletions(-) diff --git a/build.gradle b/build.gradle index 524fb2f2fc..94923fc372 100644 --- a/build.gradle +++ b/build.gradle @@ -1,3 +1,39 @@ +class AMPTask extends DefaultTask { + + def String dist = 'build/dist' + def String amp = null + def String module = null + def String jar = null + def String config = 'config' + def String web = 'source/web' + + @TaskAction + def assembleAMP() { + + // assemble the AMP file + ant.zip(destfile: dist + '/' + amp, update: 'true') { + + ant.zipfileset(file: module + '/module.properties') + ant.zipfileset(file: module + '/file-mapping.properties') + + if (jar != null) { + ant.zipfileset(file: jar, prefix: 'lib') + } + + if (config != null) { + ant.zipfileset(dir: config, prefix: 'config') { + ant.exclude(name: '**/module.properties') + ant.exclude(name: '**/file-mapping.properties') + } + } + + if (web != null) { + ant.zipfileset(file: web, prefix: 'web') + } + } + } +} + subprojects { apply plugin: 'java' @@ -48,43 +84,18 @@ subprojects { } } - task amp << {} + task amp(type: AMPTask) + + amp.doFirst { + // ensure the dist directory exists + def distDir = file(dist) + if (distDir.exists() == false) { + distDir.mkdir() + } + } assemble.doLast { tasks.amp.execute() - } -} - - -void assembleAmp(amp, module, jar, config, web){ - - ant.zip(destfile: amp, update: 'true') { - - def moduleProperties = module + '/module.properties' - def fileMapping = module + '/file-mapping.properties' - - if (file(moduleProperties).exists() == true) { - ant.zipfileset(file: moduleProperties) - } - - if (file(fileMapping).exists() == true) { - ant.zipfileset(file: fileMapping) - } - - if (jar != null) { - ant.zipfileset(file: jar, prefix: 'lib') - } - - if (config != null) { - ant.zipfileset(dir: config, prefix: 'config') { - ant.exclude(name: '**/module.properties') - ant.exclude(name: '**/file-mapping.properties') - } - } - - if (web != null) { - ant.zipfileset(file: web, prefix: 'web') - } } -} - + +} \ No newline at end of file diff --git a/rm-server/.classpath b/rm-server/.classpath index 54f363bd54..b829149aab 100644 --- a/rm-server/.classpath +++ b/rm-server/.classpath @@ -18,7 +18,6 @@ - diff --git a/rm-server/build.gradle b/rm-server/build.gradle index ab05b52b03..06d1bfe380 100644 --- a/rm-server/build.gradle +++ b/rm-server/build.gradle @@ -4,8 +4,16 @@ repositories { } dependencies { - compile fileTree(dir: 'libs/test', include: '*.jar') - compile group: 'javax.servlet', name: 'servlet-api', version: '2.5' + compile fileTree(dir: 'libs', include: '*.jar') + compile 'javax.servlet:servlet-api:2.5' compile 'org.springframework:spring-test:2.5' } + +amp { + amp 'rm-server.amp' + module 'config/alfresco/module/org_alfresco_module_rm' + jar 'build/libs/rm-server.jar' +} + + diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/RMEntryVoter.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/RMEntryVoter.java index 30b66d7db8..db7326ac98 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/RMEntryVoter.java +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/RMEntryVoter.java @@ -350,7 +350,8 @@ public class RMEntryVoter extends RMSecurityCommon } - private int checkCapability(MethodInvocation invocation, Class[] params, ConfigAttributeDefintion cad) + @SuppressWarnings("unchecked") + private int checkCapability(MethodInvocation invocation, Class[] params, ConfigAttributeDefintion cad) { NodeRef testNodeRef = getTestNode(getNodeService(), getRecordsManagementService(), invocation, params, cad.parameters.get(0), cad.parent); if (testNodeRef == null) @@ -421,7 +422,8 @@ public class RMEntryVoter extends RMSecurityCommon throw new ACLEntryVoterException("Unknown type"); } - private static Map getProperties(MethodInvocation invocation, Class[] params, int position) + @SuppressWarnings("unchecked") + private static Map getProperties(MethodInvocation invocation, Class[] params, int position) { if (invocation.getArguments()[position] == null) { @@ -438,7 +440,8 @@ public class RMEntryVoter extends RMSecurityCommon throw new ACLEntryVoterException("Unknown type"); } - private static NodeRef getTestNode(NodeService nodeService, RecordsManagementService rmService, MethodInvocation invocation, Class[] params, int position, boolean parent) + @SuppressWarnings("unchecked") + private static NodeRef getTestNode(NodeService nodeService, RecordsManagementService rmService, MethodInvocation invocation, Class[] params, int position, boolean parent) { NodeRef testNodeRef = null; if (position < 0) @@ -558,7 +561,8 @@ public class RMEntryVoter extends RMSecurityCommon return testNodeRef; } - private int checkPolicy(MethodInvocation invocation, Class[] params, ConfigAttributeDefintion cad) + @SuppressWarnings("unchecked") + private int checkPolicy(MethodInvocation invocation, Class[] params, ConfigAttributeDefintion cad) { Policy policy = policies.get(cad.policyName); if (policy == null) @@ -577,7 +581,8 @@ public class RMEntryVoter extends RMSecurityCommon } - private List extractSupportedDefinitions(ConfigAttributeDefinition config) + @SuppressWarnings("unchecked") + private List extractSupportedDefinitions(ConfigAttributeDefinition config) { List definitions = new ArrayList(2); Iterator iter = config.getConfigAttributes(); @@ -792,7 +797,8 @@ public class RMEntryVoter extends RMSecurityCommon * @param cad * @return */ - int evaluate( + @SuppressWarnings("unchecked") + int evaluate( NodeService nodeService, RecordsManagementService rmService, CapabilityService capabilitiesService, @@ -804,7 +810,8 @@ public class RMEntryVoter extends RMSecurityCommon private static class ReadPolicy implements Policy { - public int evaluate( + @SuppressWarnings("unchecked") + public int evaluate( NodeService nodeService, RecordsManagementService rmService, CapabilityService capabilityService, @@ -821,7 +828,8 @@ public class RMEntryVoter extends RMSecurityCommon private static class CreatePolicy implements Policy { - public int evaluate( + @SuppressWarnings("unchecked") + public int evaluate( NodeService nodeService, RecordsManagementService rmService, CapabilityService capabilityService, @@ -848,7 +856,8 @@ public class RMEntryVoter extends RMSecurityCommon private static class MovePolicy implements Policy { - public int evaluate( + @SuppressWarnings("unchecked") + public int evaluate( NodeService nodeService, RecordsManagementService rmService, CapabilityService capabilityService, @@ -884,7 +893,8 @@ public class RMEntryVoter extends RMSecurityCommon private static class UpdatePolicy implements Policy { - public int evaluate( + @SuppressWarnings("unchecked") + public int evaluate( NodeService nodeService, RecordsManagementService rmService, CapabilityService capabilityService, @@ -919,7 +929,8 @@ public class RMEntryVoter extends RMSecurityCommon private static class DeletePolicy implements Policy { - public int evaluate( + @SuppressWarnings("unchecked") + public int evaluate( NodeService nodeService, RecordsManagementService rmService, CapabilityService capabilityService, @@ -948,7 +959,8 @@ public class RMEntryVoter extends RMSecurityCommon private static class UpdatePropertiesPolicy implements Policy { - public int evaluate( + @SuppressWarnings("unchecked") + public int evaluate( NodeService nodeService, RecordsManagementService rmService, CapabilityService capabilityService, @@ -986,7 +998,8 @@ public class RMEntryVoter extends RMSecurityCommon private static class AssocPolicy implements Policy { - public int evaluate( + @SuppressWarnings("unchecked") + public int evaluate( NodeService nodeService, RecordsManagementService rmService, CapabilityService capabilityService, @@ -1010,7 +1023,8 @@ public class RMEntryVoter extends RMSecurityCommon private static class WriteContentPolicy implements Policy { - public int evaluate( + @SuppressWarnings("unchecked") + public int evaluate( NodeService nodeService, RecordsManagementService rmService, CapabilityService capabilityService, @@ -1027,7 +1041,8 @@ public class RMEntryVoter extends RMSecurityCommon private static class CapabilityPolicy implements Policy { - public int evaluate( + @SuppressWarnings("unchecked") + public int evaluate( NodeService nodeService, RecordsManagementService rmService, CapabilityService capabilityService, @@ -1044,7 +1059,8 @@ public class RMEntryVoter extends RMSecurityCommon private static class DeclarePolicy implements Policy { - public int evaluate( + @SuppressWarnings("unchecked") + public int evaluate( NodeService nodeService, RecordsManagementService rmService, CapabilityService capabilityService, @@ -1061,7 +1077,8 @@ public class RMEntryVoter extends RMSecurityCommon private static class ReadPropertyPolicy implements Policy { - public int evaluate( + @SuppressWarnings("unchecked") + public int evaluate( NodeService nodeService, RecordsManagementService rmService, CapabilityService capabilityService, diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/AbstractCapability.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/AbstractCapability.java index d195159d9d..081d5db447 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/AbstractCapability.java +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/AbstractCapability.java @@ -45,7 +45,8 @@ public abstract class AbstractCapability extends RMSecurityCommon implements Capability, RecordsManagementModel, RMPermissionModel { /** Logger */ - private static Log logger = LogFactory.getLog(AbstractCapability.class); + @SuppressWarnings("unused") + private static Log logger = LogFactory.getLog(AbstractCapability.class); /** RM entry voter */ protected RMEntryVoter voter; From 08a96c0ed05367e05358025ee8c40bd065c61000 Mon Sep 17 00:00:00 2001 From: Roy Wetherall Date: Wed, 28 Mar 2012 23:58:11 +0000 Subject: [PATCH 09/24] RM biuld scripts: * mmt depedancy added * war dependancies tidyied up * config improved * install amp working * use of tomcat plugin (soon to be removed because it is unable to run two instances of tomcat at the same time) * checkin to store current setup before refactor (again) git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/modules/recordsmanagement/HEAD@34877 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- build.gradle | 252 ++++++++++++----- gradle.properties | 4 +- mmt/alfresco-mmt-main.jar | Bin 0 -> 80939 bytes mmt/jug-asl-2.0.0.jar | Bin 0 -> 31906 bytes mmt/truezip.jar | Bin 0 -> 152386 bytes rm-server/.classpath | 540 ++++++++++++++++++------------------ rm-server/build.gradle | 37 ++- rm-server/gradle.properties | 6 +- 8 files changed, 481 insertions(+), 358 deletions(-) create mode 100644 mmt/alfresco-mmt-main.jar create mode 100644 mmt/jug-asl-2.0.0.jar create mode 100644 mmt/truezip.jar diff --git a/build.gradle b/build.gradle index 94923fc372..df1acaf91b 100644 --- a/build.gradle +++ b/build.gradle @@ -1,101 +1,211 @@ -class AMPTask extends DefaultTask { +buildscript { - def String dist = 'build/dist' - def String amp = null - def String module = null - def String jar = null - def String config = 'config' - def String web = 'source/web' - - @TaskAction - def assembleAMP() { - - // assemble the AMP file - ant.zip(destfile: dist + '/' + amp, update: 'true') { - - ant.zipfileset(file: module + '/module.properties') - ant.zipfileset(file: module + '/file-mapping.properties') - - if (jar != null) { - ant.zipfileset(file: jar, prefix: 'lib') - } - - if (config != null) { - ant.zipfileset(dir: config, prefix: 'config') { - ant.exclude(name: '**/module.properties') - ant.exclude(name: '**/file-mapping.properties') - } - } - - if (web != null) { - ant.zipfileset(file: web, prefix: 'web') - } + repositories { + add(new org.apache.ivy.plugins.resolver.URLResolver()) { + name = 'GitHub' + addArtifactPattern 'http://cloud.github.com/downloads/[organisation]/[module]/[module]-[revision].[ext]' + } + flatDir { + dirs 'mmt' } } + + dependencies { + classpath 'bmuschko:gradle-tomcat-plugin:0.9.1' + classpath fileTree(dir: 'mmt', include: '*.jar') + } } +/** Wrapper task */ +task wrapper(type: Wrapper) { + gradleVersion = '0.9' +} + +/** Subproject configuration */ + subprojects { apply plugin: 'java' apply plugin: 'eclipse' + apply plugin: 'tomcat' + sourceCompatibility = 1.6 + targetCompatibility = 1.6 + + task dependantWar + explodedDepsDir = 'explodedDeps' + dependantWar.explodedDir = file(explodedDepsDir) + dependantWar.explodedLibDir = file("${explodedDepsDir}/lib") + dependantWar.explodedConfigDir = file("${explodedDepsDir}/config") + sourceSets { main { java { - srcDir sourceJavaDir + srcDir 'source/java' } } } repositories { flatDir { - dirs libsDir + dirs dependantWar.explodedLibDir } + mavenCentral() } - + dependencies { - compile fileTree(dir: 'war/WEB-INF/lib', include: '*.jar') - } - - compileJava.doFirst { - if (file('war/WEB-INF').exists() == false) { - tasks.expandWar.execute() - } - } - - task cleanWar << { - ant.delete { - ant.fileset(dir: 'war', excludes: '*.war') - } - } - - task refreshWar (dependsOn: ['cleanWar', 'expandWar']) - - task expandWar << { - if (file(warFile).exists()) { - - println 'Expanding ' + warFileName + ' WAR' - ant.unzip(src: warFile, dest: 'war') - } - else { - - println 'The ' + warFile + ' was not found.' - } + providedCompile fileTree(dir: dependantWar.explodedLibDir, include: '*.jar') + + def tomcatVersion = '6.0.29' + tomcat "org.apache.tomcat:catalina:${tomcatVersion}", + "org.apache.tomcat:coyote:${tomcatVersion}", + "org.apache.tomcat:jasper:${tomcatVersion}", + 'postgresql:postgresql:9.0-801.jdbc4' } - task amp(type: AMPTask) + /* --- Compile tasks */ - amp.doFirst { - // ensure the dist directory exists - def distDir = file(dist) - if (distDir.exists() == false) { - distDir.mkdir() + compileJava.doFirst { + explodeDeps.execute() + } + + /* --- Dependancy tasks --- */ + + task explodeDeps << { + + if (dependantWar.warFile.exists() == true) { + + println "${dependantWar.warName} was found. Checking dependancies ..." + + if (dependantWar.explodedDir.exists() == false) { + println(" ... creating destination dir ${dependantWar.explodedDir}") + dependantWar.explodedDir.mkdir() + } + + if (isUnpacked(dependantWar.explodedLibDir) == false) { + + println(" ... unpacking libs into ${dependantWar.explodedLibDir}") + + ant.unzip(src: dependantWar.warFile, dest: dependantWar.explodedLibDir) { + ant.patternset { + ant.include(name: 'WEB-INF/lib/*.jar') + } + ant.mapper(type: 'flatten') + } + } + + if (isUnpacked(dependantWar.explodedConfigDir) == false) { + + println(" ... unpacking config into ${dependantWar.explodedConfigDir}") + + ant.unzip(src: dependantWar.warFile, dest: dependantWar.explodedDir) { + ant.patternset { + ant.include(name: 'WEB-INF/classes/**/*') + } + } + + copy { + from "${dependantWar.explodedDir}/WEB-INF/classes" + into dependantWar.explodedConfigDir + } + + // TODO understand why this doesn't delete the folder as expected + ant.delete(includeEmptyDirs: 'true') { + ant.fileset(dir: "${dependantWar.explodedDir}/WEB-INF", includes: '**/*') + } + } + } + else { + println "Dependant WAR file ${dependantWar.warName} can not be found. Please place it in ${dependantWar.warFile.getPath()} to continue." + } + } + + task cleanDeps << { + ant.delete(includeEmptyDirs: 'true') { + ant.fileset(dir: dependantWar.explodedDir, includes: '**/*') + } + } + + /** --- AMP tasks --- */ + + task copyWar(type: Copy) + + task amp(dependsOn: 'jar') << { + + // TODO set the inputs and outputs + + // assemble the AMP file + ant.zip(destfile: dist + '/' + amp, update: 'true') { + + ant.zipfileset(file: module + '/' + moduleProperties) + ant.zipfileset(file: module + '/' + fileMapping) + + if (jar != null) { + ant.zipfileset(file: jar, prefix: jarDest) + } + + if (config != null) { + ant.zipfileset(dir: config, prefix: configDest) { + ant.exclude(name: '**/' + moduleProperties) + ant.exclude(name: '**/' + fileMapping) + } + } + + if (web != null) { + ant.zipfileset(dir: web, prefix: webDest) + } + } + } + + amp.dist = 'build/dist' + amp.config = 'config' + amp.moduleProperties = 'module.properties' + amp.fileMapping = 'file-mapping.properties' + amp.jarDest = 'lib' + amp.configDest = 'config' + amp.webDest = 'web' + amp.web = null + + task installAmp(dependsOn: ['amp', 'copyWar']) << { + mmt = new org.alfresco.repo.module.tool.ModuleManagementTool() + mmt.setVerbose(true) + mmt.installModule(ampFileLocation.getPath(), warFileLocation.getPath(), false, true, false) + } + + /** --- WAR/Tomcat configuration --- */ + + war.doFirst { + throw new StopExecutionException(); + } + + war.destinationDir = file('build/tomcat') + + task prepTomcat << { + + copy { + from zipTree("build/dist/${warName}") + into 'build/tomcat' + } + if (warName.equals('alfresco.war') == true) { + copy { + from 'data/dev-context.xml' + into 'build/tomcat/WEB-INF/classes/alfresco/extension' + } } } - - assemble.doLast { - tasks.amp.execute() - } + tomcatRun.dependsOn prepTomcat + tomcatRun.webAppSourceDirectory = file('build/tomcat') + tomcatRun.uriroot = 'build/tomcat' +} + +/** Utility function - indicates wether the provided dir is unpacked (ie exists and has some contents) */ +Boolean isUnpacked(dir) { + if (dir.exists() == true && dir.list().length > 0) { + return true + } + else { + return false + } } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 29a87f859d..294e20b6e8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ -sourceJavaDir=source/java -libsDir=libs +DIR_WAR=war +DIR_LIBS=war/WEB-INF/lib \ No newline at end of file diff --git a/mmt/alfresco-mmt-main.jar b/mmt/alfresco-mmt-main.jar new file mode 100644 index 0000000000000000000000000000000000000000..d395dfa05d85594da5ff4059069c3a435586d7b1 GIT binary patch literal 80939 zcmbTd1C*srl0RJRvTfV8ZQHhO+qP}nwr$(4>ZUo`^(b z;Fpo{$V&l(Kmq`O0|11jvhf4_rw;@G2!M=;vH*>QtSH^@NdN%(e~^L$g#ID@6l~&( z{X<^)=RyAS{0FIwfUJb5h>|j`jHt{c16&|IeCR=s#J7+bj;DZOw1E(5m@KB6Qz~#) zGS^~ifFB)ahmS|^8bMfqV*{Kd2RPd)K#D5Ow**&_U($v(*POi73dx{gPfzvUZ;+Ly z+M6oQ%pDtpqd>XDHEr|B(@ES{O%7=5u5b=E(M53dFyRd-;!IG|i!^b0VZ!!3*Kzhm zVeMNi%aRqaf1m$15 za5S<1s{t_nTY4KiV;5_azZeGhuh^XJ?5zKS7v*1holG2EEsXwZxc@TCe`hhW`72@I z|4$?RjqQJ3<9`vQi?hXFt?O@!`?rmEvUjwwH8XWIurYD7bF}&kN#Okt947A0Cbmu% zcD7D`!A$(WGdsCBn*K!(wEvGin7A96*gOB3&R_IJ@&DM@KgkgM-~2?0eY4IvC;$M< zAG=`qM>qdr8h_V!fqz(vqKmDwg^h{GzfXwP$lAcksYO*w3CjfC7uC_9dEOqJCfSm! z2A#FQqGdHVMX<^Of=7Y!wLO>wB$3*db>6euhR%8PK@~@guKOb7Xq|}ORkW&T3FA9P zZC4}D%}qiPoXHuR?)~*w@14iq8ISM5oS*McjDFbd=Ob$X$lXMf;kdRFFWJ;5KzI9s z+(i)$+KE?308G&nR+TUYzGnbThFdXCe+XkBa05lzQji1`3bM)%08WGPn`Y{}al;?- z{wHDzx1%V*kewL|jYFLp>asoVba^|I}ROIFyY60xLoE2r%gn^wFn5KCO(^{Ps z_?P;Ts-#$Q7$3?D%rD(2Yt%$(5+hzWa)I;Bwu&2Rj4m^aD9|FQO=<;Kxb)M`+EcMv zo&+YtHka;oT|6Raj2yr2Q<8aY7@RlXo-vG?dtLF9*1)1V6n3x|MVmT7tcuf#VlobQ zd7iOYp@e3XJDHdbFq;C`-}>IqWh_R3HilFvP?}GhDSRq$x32j*)Ojlp69nzNKbbzF zUE*z3Dx}}&m8@ip(<^e#qFEa!1(O77fNC(C{Nbu+ZzF%YSnNo02F+t$)UrYffHkng z9EiKEc$ga>i@J)+U4{gvnv%X%x3*Np<{61>Kgx7X|K2XVNL?@_<#JVbIY#t zf7;G|o|8Zp4yM7A*GiiH6=(v>W#;m!q|x?dNmPi#Zi76N>`P39)ag(%yGcrW|Kbs9 z3Vn!joe(nMT+Iz9;7o)&e07d;U5l^FuLiGGuhQ!ED;>g6^%nLh9Ef)zM(#{eUI=ov zCZ3S57Q_Kj4fNF({w~598d#v4ZZRqxh?~9KSTPT{C0a^s8s|aFABIDzd zeBa1luS315igvjEhws?XV3Pn%l z9X3~E?LWf-TMNRhvVt6KBAa7WQMOdKTT=YdTE1!#v=-7i<2Fl1T{lKqIq0p=`f%z= z7^>D2(`yPg((W6rT!YOA+RFO|pHMEB#qPINiyA=Pr%thK#Oc1yh2X^~%{A+nN-QWA zn%kKpql2ZGgqw%gD@9X4-%k(JQoxL(c=>kv0`m}(6Fl{2=<0iK|@`1a>V@n zR2^#GJ4?F|*G%|c{1d(RG}H)gft_*g8y=64xp20C0=b<#?$nr?GYs_(NK*uSYfz_o z#vn&;c)paQnh%Sp_m{ObERG48N+RH1(lOZVqK)b)>|RP&^#af7Cok16|Jd0BnQ1ll zyk;EG1+a%7ckfk`&ZPFsuDN{RwNH5tbSih7_5LtHIXus9)@fwdAE*Q@%{tzl7aopR z6MB|$?y9#bu%y86A)RsQqkHRQaqp|YW_90(UJc3YFx``e^K*`AvB}u1FF@vywKydt z-r1Z?+ZOLQvrm_~a%~#CCVjg&Gv;F6=3U2b;~b&5Pd!InE#CG19Z~%~Y!HmJV_g10 zo1Rzz0IdHSHvSFu{3F5(7&ddPnnUD8k^oH{Hnd8i1PY}*{tnv8lbcrm!i zA7DrpAgHdnsdz5=2HLrZ)4ABgV`oY%b(DbQr}ruJbh|an_161&+v9n;2geJf-va*L zfQwf(VCmic!=_!u*EVTqtHe1B&F$yGh)&}Z#0SUz$c`__bb^;RyIyPF^O5fMnNZsG z_MMP&Yu;mlS}WZ9{N0c%-jD9|dn1P1M+3!HFdz2gAp<lkqq2_g*x<`=c;-*+XtRKEB7pG2XA?IB;J|GvCvZxgK`` zeE}`GIsM|MGMYvU{dyZOEh{~{Ez$t}Gh3HBOxenc1bgN(GDV^@@a5K0rH$;&$(O2$ zZqpl--xZ^dt}GtH*2+;g9sF`;%sgUJN(0)P$CGah z?SmH>Dkfnds4`g_>2kzJqdK0=QjeC2>QGoxsxELhtA-s;s`KHs&-1sBY?d;ZljYvD z1x9^TinuCGMWqwweXs!|8Z>GL2oTL|zu|HVyGuH_ORBzjYAv!)f|A(<4ux+B%tSzm zy`~cYpdjdfnY3AW1Z^LktN%j85IX%6vra2PNfQ&Xm=10Z%av@DLNAv@vYry(h(NdHi)jN!DRX&s!ig1I709|W6 zc{x0V1p<}=Xx~tjv>_TxdRu+2s)cPlMrvYR$AEa%)k$UAduS$>q$4RB`AEPnb63D= zXtN8_XzJclJr~L~Ak3b7^;_+}BXQ4rP$1u-9ON!Om!E3}Pt@1pX z3(o_2oQmeNB;D`AeUq=1s6L8!uHOeEd?;U1g5O#Le(?(9%H)NT?Z^kHE1*qjrZOV# z_P}^;6P_uhS5?c!wG|H!0~H`VYq@&egH_;p9)N(@=}1a{FM#t)l2e5 zNZyKvADD(`Ss<9T_mFT4WjF5W*UDZV>g`*H4RaecD2nfLPY9720T`~F zC&QVNL#cAA05etVO`bChJwq(9*fL$l(r7Xg>8)ie^JYX>4ryko#Ab6gt&)IFYJWBW zRpr#mY|Ysd_i$m&p1v3)iHuz~wZ1CoJ11@^PnoVD>>fTEF{U*yRZD{R!tRj&VD1^7 zP~|N74OyV0Yq>-X35puJT*Xp%wY?+e8=|kRrTS}8!RSIcmTBXu#UUf{q_6hYfVBKE ziUxU5nk0j}BhSTp?O2(oQ+(R=A|sVb=yYNAd%%(C{-W%~e)#hBSOYZHVkCIP4c@3b z;i7Wwz>v<-kRHlCQHq(fwEufZ6T?<%RESO=*a zv5i`!oJA(2^rz|2(WxDC=3X_ebCwxyF|J)JuG5hdmxsAw-E0f@aXn#7}9D^v49@wG@^vSzA#f~uAquZzQ_Oj=I%Q%DE z^7gnT`2;g%&HeGHij{1|N8}yQ7bwZwq0QlmVTn1KbYIQ;K$>?Tn^g&`T0&DT^OiTw zTgWXls4N6d-6A)ridBYKe&A7Hrw5Ti>Wdr5+ME$bL1gzt84@4DO0{W@K@?r24b<+5 z6kR3WjTBzRUy@3%a)-(Lj+0xNqz;!oBD+rRlyk)#EW5|)0}Z>!1`g2jK@7#?iuf_n zBd?mZ30zu&GHnH$KK(1t=$Li_Em!&{yA^ilwwt>wCJfAYEdsn?Uwrc@| zQ6u;FSt2Y`D0Ht2O=ApjX^9h8(4sJ>8mL4RIzTznmkrrnj=~6y^9}7aZL}V}7EPxe zPtrjwakrVAi$>HPmB)&wXmlocuUhh0?V6ZFNS2xW_LrT%M$fe*-Y*f8JR(7OMuU<_ zvBs=&>Qy%zc7>|1sPreqs2XTXII!z4uhrQQuSy}+8S%xdr|vz;s#RR8;SjD)|Fnev zX^E1>X8p#7HlIDkq$}9;qYC>$ZZQ*ix8>AW9dE@Wqu7DOR}F zkyO!+?2-MATOmNaPUJX7Ry_%Pt09+kl}T*CGpR_iY$L^URiT27i^?T4XWVJ~$s3f= z2Q{H>{-6xWVI!h@U;OxzFo9Ln*h82m=<70|G)R5OTmS5d5Kx!$KuPOZDXLAwd-ryH z%;xN=ktok?`p>VUp`J*1u>+?VBD}bmxOB8U!2a2lZ&_KHB@43VLi5vAktS;LNdUCFy47}(OMt-%&^ zN9LUvXjfM}y+Oi3+omOnhiFJ-6C5HqU%v+Ba7+!*>D>bqYEN5yf!(f%1d=XEKKj)MUJnE%Og(fk#9{tvwDq-^5! z571nqW?_W1hQf1XV{9=^XpNNbPry%&<%Q2IV?@48?s}-|+Rq3}EOX(`-3lhkWM~?E z(mbdeinh+1Ji%cUnEwnq&Pr=(Ogo*XfSuZl}ua`)PFx z0QNA?!Iw1C0l*VF2?9Mmk%4BEi6NbFHsbaJ1k)fwSN7R@!3i;(Ag>>^-S9l3fUXE$ z3=s_}T|{A28#-?Eeu91To`T^0!jwC749Oixj`SXxH>uz3q4}Mg=*Y?8dbr7fJM7HJ zogfzNK{4A?54B4#;^4{fJ_Nm&{7A~7`2GaFm#|bfvm~5C*7)kXT^jl#CR?%ql#m8-S zuulNuOr|4@oZGUw&W!3^(Ya~O{v3L%-fG4a7wZ_};tHiU5#94S=-TtyvL!`JUn}CJ z+|OE5uT0w0q9u}T6)c?*RTt|F^b`_nvqf?%4t1@lTqofA+zYPTY|ch$d}SFQs&}HNIp3XHb%wqS^KUwgNM?LN9UO=wH>AC zVo5N+qc0@HSL_XEGpN-=HD43iOx*`nVsa1iCZ)pRkX?xA_}v+MDF{xnGQH}Ur1lY6 z+CcYJq?*cB8Zg!}P{0(sH_9@r575dBZ8qLH|uR<@H|diz8^CH{fy46MMaR+qfczGtpmt#rJhp9SM*{LIDLd7=Fm_&9H;TR zB8S@|HK!N0?08xPGYFr=iE(JP`pRACI5pEsSa+nisJ*@7^sij zAbsQb#LW?mFR5mEHjf^E#XpFF`VHox7}Hnp)47MSIUUG4ugC5&Sg42ENmIU4{U$Z= z8ZXr?C|-*uS3zh3wmQiR?1L)11!Xt8`>kuSOm#d0VYzt- z%3JWkROYn$xID>?2?&{$`*?GK!1^MMK8@Bhl1*~>1P9?({n(IZztsUmm(U3=EnUVk zS8k}nfpG7mrKQjxmPN78{;&`_8yTJO5)wI75m^C7I{R?Ifo&Rd;*n;oNny3kY*1G; znlPE|V-5pht}vr&I(nUbhQ*Ok^4a4&;Bq0wkiS&1$cny5k@6NYH!|2#*L$YybdDOW z?^y;T6$uRC^+%a130f~umBfPCun4He%zMJi@?KbeagPieblFxWyFh59Gx->NJoc}G zzqp$Vd0O;(&8b(XKe(z!#P5cV>oAYMhK=xZFK!?KUoSXCeUuS^ zQl*&X4U5b3atz+ZEv!RC3xQ=(Q0`C&x}-1I%DJYBTn(#ZS;D-hX_bqy6b*@r3Y1f^ z86xfYL2Y**eHsJ8Z28qSy|#*1?NF*Fto(! zINzZlU*p+9>&%fJ=(>|0g?cz5?$>2tgFGMP>gWe6RxO`meRWo%&w+X5glGcV3>k$c zA*gI|QO&9pd#uhEpKJU#WfnZz2&9)w;WoJDDu}>TB)+)@90*MnKr17$!qNSwL9B8s z;?`*GReUYT)=%IT#d`N_J(6vI=3A33e?Z7J(80PwUUjUi&d5$<9|#zi>4BKFd1UGF z1>tMV%&yK|ekBM8h_(3eACc(I^?TZboDgu47s3!%BhR5C)Lh^gAbknYR!2H4Q{lI?MOpHt>}N76W3IR_C~B&(tg(r?_X4u(PyHHL)*P9*Eg(kT+S z9VlBHq5Q5-c5L*6E!1jq`JhGH4w+uK2eo0~&qsjiu$?6#VsHmU+>T&ogSc_JKja6+ zrU1G=kh)Sw1nTM)G)oI*lUWtmzh^iOYQpUDZ=@S}IzBjUr-FBP2@Tx+-c+n<477Me zJtf`qf_p=0U`VXf>>(%?wg?z>xjr%5m}iTM>MW#|xLrxy=8j&a_V*vHzJH}TXIx$Z z+kc)z=x6pzHy?3xT+F9J1)nYofkqpln#n`=@5!kLt@GdU@E64zF#U2%ES5|dE85<{ z=NBub*k%ec0kkc&fL7bIGaZ~m>vQI25!K{>cx| zSz@m`p%Mo*AV_M(BmBNRaDU5h;D3e$4Hk)*5x@Wdg@0hdUnbNf{$$V`olPA7+k{$* zijC8jFud=q>!G^Bld2^+4e`7@c9*h@JB_Rk;u@$@8;p#20iM3`hxqa&=bVOZ(%x{{ zY;y3RG~XhIFY-`r@p}!dQ1ABc%?)qY=XUM$8=qe{NPXbv4^s!0AdKq!#>uHMr1#nu^*c|0KH8RPqQH;I1THic!sFnuuq%~^bD7Y)JaOPg8`jBy_qTu*kIQ1skU?_KO*Br}=;hu2{ zfnx0e7EhbwHj8B^J(?8zRH=Cmn`aXqF^8PH|-AELn89@=EHF@!ad?)GyaX>?1BbPgq# zC@?rc=|NFLyDk+jMge0YO|i>uKTB$@ZSwAWa;&piD9d<9ZIN2GCynYL;q50zc&#%( z;CSis*l>NNO?l=ZIR&n17k+VX?X|SNHG5+6EDQq3e-B|Ne{h(~XSVLu1DC*o)Q-Sm z7H(JZCCbWQFIj_bzPLrzZ3P!4RyC$$UV~?ncP*&Dbr+aYQ_T^{dF~%R3T3h~BC@j) za_{N#&QQZ6EpP0#nV94qD)nk9W5Jo-5}c)7^wR7W(d|(C04Ist_s!D|&>%@(VuE+0 z+@3MWdRbCw(#4MwQi#W%#Np?LklxL6;!@Hb>S)f413;-0Q;;uzHaJC8BU$IreT53o zZGpE?9XDYT8WUBa2e@Sug7t=%D5y^uR3jSfumeU-8B<>>cN$gC_*O4B#i!z~+6-3a z_@gMi7_?cgPP{WS z<1kD8xJ9~T9r5L-{+b_qR?L=z^oT@JdA545=Rc7%_LasND0hhNqgT=Y07`=3-rsK^ z{Qxp8mOzGe2sOEcSE7bv*daPb`~}t#5A9p}yMZEGD30m@;q^T3;*b;fh&0p3Smp4T zQC-Nuzgm3ASO+!LDja%Bz%~2>KPFcU3%46Er%UX8Sg;{?v=b{a<&=pFGa`N(5)cbn z_YK5vLK&lnG?76+D0YnLtE}+TSAQ4~Yd$&Xou_rvQ6xU8^qa(8&EM}0q`ObmAK=}C zz7fZT>Poqs4V2?Ta+F)tJR5~dtIfFJUlw%8v$s}8BIFmhkjGnMiVl^g$1L}cw!*9p z$ROhw+fNEWn1Hp}X9fSxT^hh~x~83kek6fC5jBLmE_Hd zq!4mVsoLR#ABZk+WYh`WK|*cJRI&t3f}%YuE$mVB*!va}!HifjU)h3=`zIpBZ zP5Ty#I^p?Jy<{l9%$eg#)+Hj9U0Pzg(T$NUfb3wO`GsM42Tula?F-2DmHH99F7`CS zH`KU!XV9a*NW3r{xhb3^^)sNh%po^d7|6qVDUeux*oPY*ZwM2DEbWoUc*1t!g8QRs zJb?zcAWV!r*2&nY*gc>-#HDfVd`^MdM4eTnW7Xlv;(+X*1I3KLi?uEg007FLOM9Nb zIy?UNy7Z@2+Qikw`X4H#Y$LZMkL*i(wOUI>XNz<)@)m(b4(dW^OArr=XlO7|$~635 z5IseGp}AP|sl$lKfC-cLOJUTBZYf$^+}OrZHa9ozjDzg$^{lRr4}jW`0~VA6rzu^D zq4qE$7GWC7$P#^pq8*~8`^v4Cp*Bpc1s{n z-(^7rdwo70ERbk8(NBz!RZz||qUW)cRY-!2{Jbkq76)UbBeB9i4%-8=^waU3^Sa%i zY1hr5a|j>moIu;gG0iO6W4IV)r--=- z{D=@d0bN{ee}Xg=NlmIOifumdpBm>M5Nt4!;F%*nRQm)qCMgq<&Br37@v9Mwi|Y%M zpvU@?m}KE4vlyK&!BFB`(XGHn|A15;m{Ser>+kO@Obxw*H5}C9<_vTj8tMBJl&(6zL zpWio9KlLU=_D~G_?&u7=r~{S2rkG+E&X7Bh8$B7!t>i-k@)IVn1bzgTBK2@R`C19c z<2!TglX~hIl!SNaTnV}4LrAC>rX=PhwElcl?UcwagBQ>4lDt89O1El6ol}^ynOKj` zqwo}cPU$|+6cfmur|6i~GmdS%PgGRn=5degnp4M>S*yY+dNb_Z%QUK)cv^1eRp~*x z?Ns^CL07G34$n1L8^2rvZUzmb@Z+8z8Ksh~Q&hiXbD~t5Zp(*qO*mPU)*bP>qu*`1 z#^{}-J&##8j(*#gn~}jPCRrmItL&EMHEwSIJm|ge)P83r>!bJ`t>u%4rjrM?mdt_G zPQx0WW#xLVysQ9?P75dQDig>+UAOeGolC%%$Rv>@PLi6Z(S5b`EHVUnQ>Ck?-mO2S zJ{B69nAGi}J;23zdz2U(rRW_|=Nx_17+M*t!adJG0%NJZ^uV%;K6?Y_R$<2lL+i{v zO!Oml=>gpeGxz-c$KK^T?A{fx2n`|iIH>^EAO~!JIL|CQv7^3a)x%r_uB@nQ|=VCNf%$Dk! zV&=3;TuI((u20XoqAbtXRQ1Ix|2B*L&IIF<$?Tq!bQO?`&OP?(_DTX+{dKy*pE8%P$E0I~a+10l%Udd;HdzY_Ql zNf09*@x*z(L47Qd$QH>Yw2RD;DrmARyZg0{nYtsKZ_P&Fp(vf?dFkevB<5x;B(B-A zVGqcnL9>_~oe0;nVGPpjQ_F!=CI=}k;KJ=;NHS|#xCuW%P+|V0AXjKFDbuh0?ct}v zv8Ai1NzBJj#i0^ZB5FND#b+w*F?jRa@F?Tf7(}fyo1c>HvLhGtzR78+qksL&S*fpZ z2b}*8oTB;@2LHW`?mueQ{&Vrce|tuXQIL@XVt~&fY$+<2M7Q)`%8hjAfz8J=r={W z$B1~S(4D^qj4^scwL>XS16Tw{gnr!vhn+&G=tOz(W^UNBdb^=zi|*gyh+&QH$>iczGUS?vWIc~ z0yfu-qy0pi%TNx=$%YUcLR{HZ(S(jiMg*;{{LA4I&d6Phjf7<{W-(f8C^$@1MQ;6+_<1+n;^V{Bv&Q{41~i zFMAAMwg@Gi5n_>E>4h$3Ktwp*?_?-h6LC6qJ$2Drl3B9w4 z+XpV2^(}IA_A_-E{oFjI8#Nz8i9swN^v}#AaJz}qUOMm4i|Q*BXxqNmV7tyxGRay6 z=O+^_^eaFO$0(9Dgjj79R|)rsiQ!_$%vNkETKZ~|xx;+ivvE|Rr^GBY=p2nH1gw+8 z)_dK18?Ewg?W8TO?WSby4Qo{ohUshq-*mxUpA%0z^v)!rv3qOVJ4mHO-qZ~?5ed!O zBP)E4%qB?KG?O?(vAnb#5k6+O%NV(UW2mNUjmMgSDaWvWbyBt3m=Vl}lICmS6}Qga zr$C@or>R^DRce)x-%3f?YN5&#(MFrKaxXe)j$<=d;zs;q9?$3J!rEeVxaaEqLNIa8 z)Q1hRB-B^q`XWO}I92LK=5YMU5E%(biepM8-larQE|z&s$a$V`#)g=z=&HZvHqc8k z#YXH8z?EL6n9L3H@Wm^gl@wph*=3x)D-U{$sKom ze8kwwJdm(O;00pD{8m&{agXpe^@NNtmr_xfeno?jup-{VGXeX$m=zTch;M$LD$hQb z1BP0@ZAcY*zS86jttOBTrwf3q|0!^Du(p*rGhxCUJK6x7@KDYALyerd(OngT5#V@U z`cR#b!%|B7+PO_>j(}zz7%N*m__+);SSRhaAe(~_ARe!0i0eLYsOCxv2DjgTs>T0X z*?tcCyyd{3B6enI008#?lR5p}m|9ddoUlaTe~+d%Cp1UIVUI!_0~i>8sB<`o_md8c zv$BrM)_cd>2R~R6)-`RSbWx4TLWBGUy;FXGR6%Gch&+rae24i0sU(jK9OIvpOrVj{ zA8t_L;_2DiS^D)N{pS6BKUD_^eNaT-X|DlGH)$XZOE*D467~R&J0y(GAT9d9Sc)ty zb`Pl=Ll~1mZt5TctA^5zDu8v0Y)p9G-<_-xu8$z4&>d^#=&Cb-1-Ywy&njdblhx3j zLz5XOAhjhcdvq!z*4|muF{r`hTyWtbMiphw#EXH3%O>p>d{ACD3xyHKl{=p5*9C2J zq0&3rQfQXG2~8J&Y(Q}V5v8@;o^uwO^0XZv!`jJyt*@0LIi-`FI0+Fw6x&Co}k?a@p9w3X57 zVoU2d7Vmvx#1Aw|zwK-^ksF@7D-4*Vuxx%f*)z0;!0*hkcz z%2gFqOmRfXU3A1DTmFGPSKa}!OgTHXiOF421bd3o*%$>Jg)PB|y&YIj)E!b)8C_k) zUMeJ_8%Qc7CNR=1!I-^8PEnaXKC9%T2N5fAHX(p;6?t2=0akK5G?J^m$`A=}{=j;d zGR41#oT_~+{sRwjGTwSsfYOWWR)vi^C$PH5n9I0@&ehK*wu&*h%!ajbCoD&a9 z8y^{W{;qWDZcX_6>=DXGV>S(i;sBE!R+i*xy%*t;XXX$YD-HzzDlyI zK|X2$=%nwMqGgoM^GWeHir~#HlPQ33#kG417TCPp+WTc*Z^>UFwna40qYF}-5Scn8 zx&)g^=TreCKc&nipp)k_&ta3{#ht;Vz)3rWNP-dJ^1W}b-WJdaefGtkm&$)W;X>=f zKbn>jNGLA;gaaq!cV=?HyXJ+3wP}AOGjI+If2_y1V~uHJ1Lz5+SEB1yhNx7@?IqCXml$M;ACZNurUNg~P|P|w7_8Aa)(H5nIYB#Y-3vGVEA z{z$d%ky}E7GZO8~k3pMIyLbhEW=c*i#Px`V8x3+vruVB`?CtGM=z#BE+~S)XU=aEr zlyU#ZHU6L6BI7?WhVqsz5(7Ms?ILxTwS@R2%{C-;d~v*tx^&MZYFYvtR)+k{u7rFjOXbje$ zd~Yh~+@KAV8wTWr{HX4hU<@GiEmDbaGtMJBD#MVbniz0iSRD}p*k;_Y`X|wjoY29Y zOsNeIrWJ|uqk6Crg2#;*$VP%BX%;kA%k}LGaOhX-EmWwX8s!5s&WrGK;G1A$au?_R z0bpG5g4t+y2jGzBX;U|gP-~su#W~E7W6dtlv0VqABg=i>3#>@|d}q@WE&P$NC5l!# z)B2}lzI&pmbK@^=6|IM59UM2_0D0;_=EMoFyt0Y-@^ODGbH6LN7ZXAuyV#vSmHHOp zH&@J%!?nx|<+<)7r4GgHx8?ZD4az}03D7QkBaE{h?4WBSh?umh4s(M(Xfp4~kJkpq z)H&XoPM{AAq0e>4&y zhmuD2grc2Oz0n@JM68lP8`HmQo0c4s z0P^qB8XYZE3Lr`plm>l@G#a!A!5TurWq(rD2aokw69=xX>siDvwM_nQZuGw;*O;^v z10EX}WK3tLXEW@2oo3mduHN4808Z-@1%9--s4edpqiQ5Go{uTRl`u2e(gv=oa>GjP&+w|j4;at_a( z*`c}R^7MUXDiJbPkCrK|&o8R*y5s%^9@2MsM9)&&>KasS&ehp!6OrP`g@&ePFt(Hx zazy#*?0D)zt`_*}y3z9iEeN=>>b5Kv6=oS$&d$0vzd9Llp(QC-5TtVH;1za_gSGbVbDctx1N->YTu$7Jm)$3eu4A9$b)j#{guoKZmBAju z;TY_Zr4jM7mFL5}!5FF;N2SNDc`*V4W}aNC_+{>wL9`VFx-D(?`(IovUGQn+`=8X@ zF53TcNAW)#)uEiRO;CRSHcr~ek{w8l*?wy2g_N-ia(FkNb< zkj%K2Wkqr|OU>9|6}S(8>t}TZxl8M)nbWMK~?}n!831Bi4&W9u#b)V zyt}q_ZQ3f%5}e)e_;UKb>;3k!GyVN}eguH|;D(l25NU59EeyYwh96AtwN$e}K3h5O+T%CbAvT#r`-3;;lW5=`A=c z;GKB1J(I^f;^D2?Z^k~u0|XmUK4b>!E!*ECyG>RIn1^-`3K)hFS);I0ZLM#9z7UhI z1{;GOd9l_Lin!2Yzyl9MvAg?TqXZ}BEVEF3BS~I}j%8ujRavo08hTpk&87y&$jOLPd-lIU-pUS1N~ zy_0Gum~A z^BGX#vx5~?!0z#Fqw@e)jl&O7MH;CjKmFx-xchga;3&o-N5aJD=~Iqa8$EhsR)p-I zmh6o@T&Wy&u^%Mb^JCaYo6l`*7yxp+jYY28q$Do zOzqy5@<}!20=Kw+b6rIp3nfH?lR>{RGzZ|#ua4_n+#LFjt1Lh{)3uPnU6G8Be zE+P63j(kz%BHS}U%pDd7=M@DRtN=w29iU~)=;$s;p%Cr?DQt|le3tm15p&0(5r0KO zeOp2F4MZXOMy-zMvpP)gLA>__^XZEYO1&2a^Bp7y^BvJamtYLg95ByPhe|P>J*tJ4 zaKJKzZbEa9&^r%pCAgQIw=CBuO5%n_wI%#Y<_{@R8-`L5Aq-@X>Qfktud`1+s+0C* zc-IXXVj42HFfE^xlcNl`Fv%ILtB-W!D!hK9B5l!xS+>5J=rfq~F(!*N>_aRz+DXqB zDt**GFP{WOq3UX9Y*x+*SqfC)sL&@;llLt_o}Rqa2t9L$E>@-@N#PPKNY>k)Sf%SB zk+NJEZfU2shz`HTBQfW}E-gHSxfs9?fp?MYZqZ1L5Xh;l*exI4+0EfbJR(@B(U6QE z3#-?I6-mho#3Rmw3*Msqx|F z5_ev#OEXRj8uk%VarNjX?V?sWvs_i3tz)t%w39-aJ`JTVD|9C#^33lzY+o5M5G1DRavLz5(<6z%L7C-)tb5Ew z<4bikCM2rmr`HQx%t1BB>Lk0wYX{6OMW5BfeV&Qt;LcEu?=q*!gw8t4gFM%>e@`UB z@`=JxuLQNwvDGM!?I^*2x4O8BG3wZH{1rCP!$HKFF>~d?TBj#WZ-3XEC-WZb}{)dAgF4|HqD z5C(R1&+D{8^hgZPGW_+6^Nhtal5}{a$3`2ivCnv*-Ppy%0~{4 z9#`BWN#*8FE2H(914DOd4A=u!5LfLqCM9L>Af1gS){ z0pAwM-$7ym<_<-^f<)W>4v@7ZkG3>V^=qobZH#4GLn5vzNgEJagLh_Ao4Ph2UBhdP z#WX;<22`8U)L92#1@LpZ8er5Cz}5z1 zZrp*jhn-+N=cSy#2S~GGW8!fidaf-aUA%5UYF-w6(YPX~ko4}zZmf?LU%6{Xu_g$o zHYA>r;()A6?|7l(ZCvZU>Z5Y8*N1NV@e7a>dcUQ|9e%i!DP zNcNiCbpgTwe#lubMO`nceQl%dUyHd6OS~>u&;bBadHMk00%Cd#Fwpt{*b^?;n&{!n423Sikl9)g&O=)Lq-baK(K2W^X7#*?D&b|#XasPof z)gvEjMyWp2N7b%gai{@V`?zn(on>Lw-TuQ0rTWPq53NdluFtANeQd-N*Y7mpn!#q$|=@ z6^>?oLEXyRlg{oQ4*_mZ4;f2~L3gK)A-c1psFb@EM|dcE<0xtsAAV8#`d6t@xyMAV zc^V+;j&bQ#9==T922t+zi@wLB@kO(FJ`lYtkba}1;l1^9{;A}WD}R?j;T!p~Q++^1 z;j1~Uq1+vP;vJfNr(Ux?i}8^i;ZpwMuCp(TbgOV@+q%G|y;X6LANf@N3XFWKdKW># zlW$NSpnKR)Q~8RF^y=u0ivr`Z2i>|N0L6RVC;B56VDKZ>U`);|%}ZA1)ixK}TkYU| zt8U_MBJAjpqn&|PXGY9B-2i#*tnjkM19wj&_SHHfY3P+YD|0J27w6X1=DIn0{`8z1 zxR&PCuuU#btjw)VD@?4bD^9jQ|1mW)5oB>>Rk{d}EA4FO7NsY23R+7lT1qm=%@g|X zFJ7{)tk{~DhG#o7Up<2xOIuxXth88}*G!C+9`FN4cZg-ndkQ>o1R>aDK8(c}+^o%Y z&?+mEZWL|TR@zqC)>hcoSnpwbwpW(tS4kmA?J6Ny+*sR;|3A{+DLB_CTGx#272CFL z+qP}nS+Q*=E4KN^wr$(VO3vE7&*`qStE;;9>5K2Xn0Iqtj2d{~XSACh=c^LHn4LA! zawC;J_J^Oe!q?Q$6}l_SRP?rx@zkY6L>lQ$mYJu8u>ablmjTXYi!J=f>Wq^YfqgFg z;0{%Bmcq`eDUH2OE|Hx_vDPzKl(pGdK93{o?0P1xSM9GHLqS>6abc+I`7Ha12dq+ zA3ze^#EzV3EPm+i5^$@|w~oIv5&Vh$OK*Q|#F2Tzu4yszCbA3(J=S~v*}Ganp)pUz zyOWN<^Knulf*0Ktgb6d8G;~qI&`J1-@)qHd7qZbwN}<>QFL%r^U~Xcs=Rr z5_Buj92LeYJ0dvZiMWv$+M)DDrnYOgZ+l+$zMS7m8$7qvAYSv0qN1Va13Jtl6Qo z>qfF{SW5UAlR_{KjT~|DSCzWu<$!rj>!+&Zp$4Xe-Q3%{rbiJ`p^#O0z;K4#pe_H$ zOoFmvy9efh^mOq^8h{iAcakDjs@R|nXiKK^q56P`bq#x8PaTkwe&M#y3L4nWS5C9K z*%3jaL<$*5qhLFeoJ-U8H-Or53jH~>tMf_KdZtJ$8i_rgpMZ@4%4Xq2^3`1us zI&8OQSl%&vincuzBS($@=cw&*DyEdT_%S!ib0OQ>yu++}E02lcwU&U)c-5Fb&9Q-% zX2*(lXQ9Cc08zh~Ih@sS>)0N1F;Sc0W%1HX_K~dju|b(%tpe+;IR?k-iS1u709&sR z641{Ce-puAedpCK67snRi@gQyVO5XuGH;cv=itY=ED}itzn1`k*q_eYj&n1=r;oXG zSLG=QsB^lfWqex%AyL^L(?~9BlnPO>eQ&b?w(8dUvaipz`AOX!??+zwbVwY{U z?Fn@z1J9qk(|NOl=ZEy5^wb29ypiH{k>D<-{0^ga2ezrX2n<))_h^5s4c|3M?2A{R zc>{$H_${D(hXo%HBT&e5y++Ft5eQP9%?0zV3b4q$oxMv3uu7_%{4ub4!{x8rQ-TkO zWuSa33hx`BVEdx$6+KrAf(;30cvQ6dHfQ_7drgm5*M<*>kGFY)*)+Bn(h)t(?GYc@ zv5KL2gM>F27O;M!@YMzg92y{*ZB#``JUV*=k@y32oKP>xRB2ZP(_ zZxs#TMfp7fimcDtsfO!{$mEzQ@X!t`+Nw38kB`J zSO{J2NR+dZvL=K=YMh2@Bwq`9E=pK?JN3l2qz1_wBq&I}vm68j;!UYjh7SlOEpdbm z!`-ojJV9=7A+T1{ymS+T;e7Ou1|csH>>wM^XfdL51)c?^)r{ zkJjt?UVi-6PC6Nj9S+RluRScumr?k$CG(J+uf-wNl?PUFISb&p^E^jxB&oB?H(ul2 zRSXDd;&%CZq2&*j&gp0oLzD2Zc|8#slr#{VtPElkaXDZ0P6nPexiim>&oKX~fEOUQmVz%ost-~Jaz-F!&@JaSX z=)-#$|2UPXY43s?>7&*GO2duDJ zocx%`uW`fJb-3rzMbo+uJw0y&?T@yuXvKbwtQQcDxJOuMc8C|!$EjZwvtWysf8=*J zcJ&BOJq~|=l?Zh?CKoIDrp#`W^z5Pgx@tP2xEw83orlhc?M2lSl31n@yuk9wB73X$ zv@!4rN)1w()eIfL8*bYKnm2Y}&FmR=n>W|6T%H}ejWAAKk-V0C5z4nXBDP*+Z*X>G@^*j8 zvt4#AeDR5XABjWBsB@evbyi5DJpy{N?)9+cFM3ig2x-%(bO;>0b&{HUlAn-CN4#?Y z0n`~63id2!n%&*&^@$N8sCu!3+ur!<@<&bJgBdcN^mb%u+ch$EHe)6GN%cf|E61x= zW{|f9W;ixg12YpPg!;!6i6ka#vEU4F6yK~8mB0b-=R^TD@#j4w>#go^`EPIVzWzpc z3E;5LDo9w1*}xs)ZjOXY=x~}Qe!Qo}l)uQnXFp@f-;~Ity`Jn5YKKH8 zC9(NYu>4k%+ZaZ@rX+5Yw(zwZXy*T!kE+I?-5E#zqBL$H^OZeHwR%=hY*^aTej4iV z|CBh`K^=C!>$LuzW+<27cjqa-w}W1sAANGmZmhOH)alBbLQpPCVc3AqtQnh4ke9Uc z)ZUScn+4mv^i*Nx?`G(p{F=a(zqoGR<~pWOBY?zl;6xc0_TAP5Rz})KfnqkiPhWdL z{<|Y&Ln1p1Z~0z1z$)IAXy$U0`x8XSvyu06UqGA1oV=IjiSal7epCk*uM%Ck$oOO% z>XyqXs+oPu%oM`6dMw4Nl5cRVZh%JoUb5q5o2!0bwH<;Sq@VQ;@O&rKDZVR62V7-H z?K`%jFL#Z&xxwAy(A;ua`4?H|vuEkMqQ;M_MZnx9AZLSPeM8AQU~pwe{90jSW3%!v zmCmQH^{%Z=UHJ=Lt2<(gfSrwj*7p7Z+arR&#@44MFtr(#uFW7xx_}rZ((&4=uont z`A5`e%^^!9GW0K}!8wRtX>9XNUJod0ZfZn9 zvtLzaP=D5tF_65W5MkHn+EmDX4|yPqSVEz@_(`=yU&<&6PhNXiP^i=R59fF@txk3P zF=#nKchZ~DQ@^f!@(JH#72j#6uFa!2TqK-&$TcdM`_~zK>Vk@xtm+Bn5*YU^3miDi z$Ku_QI;#~Aj@xY;q^gXK3#nH=V2=i{DN6h3LyNU%fz<>Zw?kaiBX~Q~l{n#D4M4O5 z6ykFu zXYFIXQB@!X+k2w>i)ZNB7p;gnwHXi(!>b*k2~coT6rR~jjjnmm46eA`!M>4c)j|2g z^%Z3W(Y?T$gq>P~his~#x1EyNdz~oRoG8JD^p*WEm@@vhN|Gu421M*tBkWQuza$|< zIIvOSY$Eip$Hs^|Ct(B2l`OHb3;I%n{w8A%Z^wda0f9oU1=JJb5VdX+i{gnv^(9Bi z{>BUyH!<>XH|lGu0UTGI9#?I5^8>8ue9k+qkA)oHSF#fvnHvTZFoPjRggx|!2JACp z()($z!kQR?PYns04N+R>Y9SptwBDUDhiLYlNW1pb78;2bMy3^^83r9B^FvZ9ztHuG z@p3`C_RQY!&i~T3YirL#KT;W+(YmiSubFb21Sq&98qaLs!F&y%!^h-lm<|1qe2q8v zvkYyEppHsTpJFO~>MFY!`ADi^H2txGQ%hFDUbo8m9K)46a$kTh4T9(}*Ot8RT7!;! zQg=ke7vPpXS1WNb0e5Q`Lba)vq-VfuTL%_5fzswoGrj5=Grv`DZ4(Z=<)s%^RXx_+ zO}xz}00Va@SiBU^h$blByMxvY;{aH9qtOq|3ZvczV(x&i?)Nw%?jRx_fZkF1W91F? z+#zl;(6z@ld*PWu29VQKq(C+g8dRu@u20h4VXw;txg9~Qkh(kk#k1dx3`tUqMA zwRVHl0d^&OJC4q6)hKAme*8nKFOt-AO_l!UR>UyHRRjxsU?Z=%!8<+8lec01Q+qEs z;QZ}8Dx@LNXRPX3XTNUAKCN^Jk+b-;{1}PJ&2~Pxf>qDiR;@ADO2aVj$fPUYK(JxA zf^48k>eV%UYaG4h0@1iTVqS9rYE|`Nsj~K;)_q2;&8$NI>{(d&Ha_UKx_f){(;*@^H=ycV%x*yTZ(P!`Yd zSNqtW82nrYfJudUthnL4eThXFA+tqSc=1d6c1%wdB6*`fqu~|*H?9VBH2IKNRaaT^1Vn6 zt1q#fqTxJ7S?M?Iif#6wmAw~%uV95X$wh6BlQ1})GLnV0mtgyDMqrh!Whelz84$;s z+s>x658mYqhhpWN9V87w@X+zeTS>`GG{ZYP2Sdt^C$iLGMvLkL} z6#zP{8e!9@i>BrWltr}Rk7>;J5L1bMrML~u4T;v`wnrPA%&_~|7UT$uctnr9v~D?| zqf+b`&79q$UmT7mJXVnJU}QZ#vBB2VR=1ek73!f1d(&qn?H2nm%B{6`qdOQ zPSiP75Y7e!opck#G73wLqBYMzn$l9)230-M8G7;cl?8HQbQfWfN2+VmZWu ziLSb5cB1pw)QxZZjdMrjR6NDV4>Y_clJ`XJ6%$VM!2W{Ca7cN`q{?y=swxu8NNY;) znIW>W=8>Zqoo?Lll;HyXOs49hLjI27RjfuC!WDdIivEa>+pqj^N!*!lD?#9uHaZ~y zXD=~e6|?V&%~H=_s5%_UIo@ptaXbl1M(Vq~Wuu3;-S{ZG$u=LB?DdP)3+4k>);-?- zK{dx*K961kD96?PPPciMY~q}=xSx4uo{r_00ux1BlJNUMG0GUH#hQkrf0&TTRC-Gw z@~+Io9^P#c2a@xy&?EvM@yR2yEr7UemCctJ(_LPi3A;(Tb_dZ>xL(HmlJ_9KFC z+*yD@d-V7J4etI=h-6Q0No>PEku&N)FUNo1JMbSwod4w=NR5?402V?DGkC`dAIcG= zt#nu3IpjBBZ@q;=`E}2FjUYSxwh1-7_IWz3ap4-~mM9N-T3_Apl=`L=vSONSgMZifEl)UjZGyH;L=i%~Q~ z5cd&@69Yx)nzquIjeX7MBm6+GB)+Lz<3S-WGx!$ZzJPcH)B7h}NRn*{P*aH9jmC~3 z@FXww2>!nd&a^;s_*~nAw=Em?7aTVIO#tJNz>{!Iyh=b{tA*~ zVJeb~Aacv-sGFu8wjj;=QOjecLm_dgXZ z*-&Mqi0ttMU-c}vGc!|}nJg|d{O^aWqd-w_++j@JX`~O{Fo*2`%gA}0m>XvAuCWRdz-of2<-US@o+un zYmgstx7sg498wXxWe&?vddJ>rCJt{vwwz^3 zn{2w#yPJcQ>0)~i(oOmT{?xv}TCH}+^s8{QDkW++B#)K$d6jvQX+|9rLAg}m;O01P zaSpp~7a+-}W%Nk*c+xs}R88kqjn_1*?A9QlT(#2H&tt0a`AQ9V;pxaW+IwXtPqfLTQQQN&}E6 zU2lp9B)+uw(EO=yaQQO6K@5kUghqx6gQ9A}Mg|8!pH8^anx2&RXfoDo*w3zjL`>m)hr_5UGeKSv`M$ zCmuMb`#)Gh=uHZ&@YFmhdr+Y6sln8bY6P{|n|xH;IHF@{o7q!P7I-iOYI*Q*l-LP6 zl~K)1%Tqk^O{^`#N-+Oi(Fy*t%qV;4t_%Z#Z(mtX>p^K$Zn0l4e~(k{ zwGf!2b=B!+_$1mh$+0+e~{7GI`v!O^oYbIbWeQ zp;H`PVyEEZMP0{9v4EV1GReLiX_`Sw)7)zl<^f5!-fKV!sx$)jH!o&!Hb%1iy_$qI z5Q@OhwT)N7p>KYZ@ZQeugF>_!2sG=3qzKq}3KDcElLCaa<=h|vhOuNCEMdbo5LTQ0 zgC!g^#&TT~YC-$NIPf3w4|VUs` z6cJ4{l-FU#zp$};*z0t}BZ>Rm`e4FOC}m?5#p%%Sp=S{G#Mni+g_!%itP2NM`b6@p zlu5fLtRH)jF?kTF#uCP;&`jv`izO||TNEZMQ1rgPGw$TR4Kf}DQI3=|HzM3q`EE0Bc z{s{Cz(@>)P*MA8|^Cf==tN(?gTUa0oq+zQM2I4_)*WT&+X@5-+(^9-M^2=BtW2)6PBaH5ZLtNXZ_6u%qTla zPO`K+;_8EDSWHur8L3u(<3c^mC^O>rqcTp17#YH=m}tim8a#1tjig4HYo*%h_rpV9 zv9g+z>|7T0_F9W|$N}foW~YmPoF-4=%u8iz?Mp%cnaW0~n*cba*Vdb-Cl)w0md$R< zQdsp`)4L}#hpc)Su}RV|D|^%qzF9ntvG}vHn>eN%g(c7gUBln9np`t;Im>c4n{0Kp z*v*SsEv(C$igtR{A=AGZGiJzT!Un?;c}K5#I>jxdM_4=fj4I*)k*CUd=+8?BN$GS5 z9uzO4Z4^$1o?#wz?6a3~GUnHmCTBdp)x0qfHN@H6-IO9Gd0mI$`vku$$dcCCp5rcX zvX^^M**^}Fx76H1G8UGxaRp$kY78}KV8Eb)&Yxx(B|Gq9JZrLEE^O^lJ(#(BBRr0N ze~dzP8_CaR|IulLb|@Md??j?<{jL>!0)}6nDnHfL;dlg$lSojV+*&vnHQL-&c;jib zY&+KuMji!+qCqgCURc+9PXzSSw9k3>VM1WeEJF?w&T5hA9Ada3_t%a-#A-qCiV z@`lI@#Zr4>mt)ST8oMsLN)H#Ab@6h@1pkrYV{fUwFCrw}QY{FM;Zu_OY`3ezA$|pM zO5HdC;s|CngAo%6f|yVm-9go(JQt7KR6XgH%m<0nF&?97hra+eOPaIWZu+A@sh@lo z1Of^UT?PUR9$xu#EMJ6BmBG=~Il|^tgIh~Hi6rt|l4f*wW~}<(^x**~|Ti5Ec_ zq(A8ka%D@_!Lq7f`8;^x$=G;um)l1wV~%d^pLo2g;x9mwHRo9wh9u-RwE~f-zy%v2 zKE}4hEn1{|1=jf!mo~f?ZeY%Z-)}{0*nI^nG-5D9Y2N--PPyWPdFH&)Xr$L=8O~pX zG2l>2K}eO7lRGyYPY@p~B9^ZmAYek!U1v?fU`fC$l7VQOBMqf=qJ0}CldoL~#%3j$ z%4g3B`r-t+&?N+8vryAC`;ZB($U0H>g+w^`*D*U{?l=$17c8OtqxUmH#KfD5l6odK zi{x269LT$(lQZfzW;U=C=H8#Zf;v#q1Yv7E0R%5Q6lg}X{q)L|OX7DV*!REwLFmly zVbLRdgADipDz4lzCwXF7C0R@%{@(mB7V01t*iPUfo?*IJFu^ys-O@{3449{p-tcvh zz?-BsMp-ULT6^=wU7EB!t6h=F1SH;wCeNQ&pC8a&2J$0R(vWvJS$90|e<9yrC~6nI z(E_~u-IL{rwrG9QY09Z1I2f-=t-*?e5JQFlc?nN_9%zOMPn;E@yKqMokvvxzd+2N5 z$D7wKSJb^u)(|I37>xU=+a(lSVfa}(k=EtuAF!X%<{& z%u9q{m!*z?`%UuOc zLe)7xbak_@*tO>p$zozpAWLf6i|JhIKlr#6vqgkIsiK_?-p;u{uA*GLaE+u^kh2OE zbBmxjvXfGA>2!bUKuF=(Vr`*fS8<9{Kmk0AFn>C+&z;PnjJkx$gRzX9**GN@REqs5 ze%Q(?wR8RWeNSAhgqwg;2T$A2d9iPj*-?T=J(=PUK^TaI3(ocvu}z7EMS}Ie{#yxl zdv4+UY%*l;TNm*a34kCc22RwOLmw8m!xnt=>z-yvrt|tvTQbQQ680*c0-UlDQapWa zw&V_a*3k}f1sy%1$OF_3IYo*R8(Js;jOLZN+@4x8U3CHqHKmX;`6}bN=W{&>{ zyIG@YC8)cM_Dv7koxY!lB>bD)QgVnh(9{r$2|Fffd?=xw5(%}EJb6D%7T0`)!-3ec zrd96RT%&5c*{-=}V}*?wteCxKL))gNM(@hrqifZEWug zG|%Ox=kbl^RRF9&s(A(w_Euuce>_BpK-sXaVUrM5*Paiiv_IP52{YOE_JJRBM&;%ulqEAfyG8IW#?X!%G-$(=yhJQ(!SH7*ZY{SuOQTSBWV9n0-CqTUv_VmIqw$A z7wafXd#8=>V$lAP29<9uu7eJ$U+UI;r>2iQXnpB_2~@w7hVxaww1y2-ztjdE<)8K) zQ0{M0z6q$mD-R4{^cC+Mz!_i!1#J)`+yFQaqsHE{0|8*@ZeqmqGh>RpB|ao1^OBl~ z*&t}y2P7mWT2~LnYdxI}4489~=i57rhnE+d^Q$}O#j>ONKm2_>M~H}BK5`_O`1%Zc zS64gDJ%F}iKNW^@+$plg{>^PKdyjOm3+u}#CHd>u=ciT=k3?OrHDspn?e95zk$7<8 zn|3U?j!!od^qA1D1ClP+5GWZxjJNL1&x!`XW=}3B4y5}UQmDd=6_xu(F>3xQ4lcsy zbo&6?+k%9SMb32vT62V)Uq&^&GjcdTZcOILT8annxnekeZ2Mmkl7V_2vWpw3RiY_l z6~s@#2{|8jRLuDZs-_4+eMv>v?8OG@1MAEn(FE2&1y<&WrTpk`viBP+sn;@DGUC*@ z5{C~iE?TLmc9tUZX#A)P@qBz1o5tdZv0)!t;GhvL;fZ>f5>2KcyO5BdawQc&sQR-K z9}>qi5%9b`jZVmZL5EQP{-0|M$|vz~S1i?m`n;Im{6Dn`^u+=wcM%_d#TETGr-WS; z5+G_bL%f z0{=omP^usTI?>lE<9;k`YWTRaUgucvp;-5^>s>wZAu|9ly2H}u6J2q7kmkuratt>Z% z0n3VMX#b&~zCpXKR5l>0YxsPH#C8QR%hIHrU~7<6wKQ(1SQsycS`GW<0Oj8t9y-0^xG=-b)uu>($>u;+ zw<;2%W4$U&=T~fz6f(XXYqU48h7Tta6P&yTUMol$cVic~m*`lq*7ZhuOqn&-gbgNd z3pV{IT9zfUjh7{Bt+{It&Z;))WLUA4Tk*1V=Xy~#S&!ofcH=O?CZOuK?zUByCWZX! z5LR&H`JLtgKGjWT4=lL4S^1!NfUH9km-5)8L!O4q8sW*z30K8&cPM?+2z=#F1?5sJ2%LQ<;BLYXz3SwfIyJ}IupGUBr)8y(&9FHY2=Yk%G^==wYG=X`&e$J7qe^vUCqS+*Jts#LfeJb`3XICqSG3t zx!wBIRRk|{v4Jz)M>V;{$E-WetUHvqb}J21JjQh?Wf`SBhm$X}E<8;)I&*(L0c6tI z)egUj*+U*gb;#L%VuKf{u7d)_f$6>xkxtyN7W||yUb3S6v~@shKQDyk(8j?XqzSs1 z?7O`q0#t>HNA0_tOVEVZF{Xg)M1Iw|?HD34-q>C+Pbuv}mp=Cz*BK2qS*a37B8UU` z)1OSl?BnWDT$m`0$gA;z3+WI)%#{V@25RE5Y9v*!&N|+^Plk$Kw~1H_AFaV2y;rPO z7WvXE>E+5o%3N-OGU=S91Vf4qll}8=VY@vZjxRa=QOV9xCiC;re07_O^RVtrx8W2< z+U+2b%(*c3fYhdNq3N1R(N?Mn*W)6Wuj)u}`XyGmA%YHz0=?Y#vq?s;%;7iEsdmyK zucbpRFf$VV$od=Li0sVgU*oSkwX2&u_Tj|Jb8pm8k<6Zm(EqG`P=#V~Z9 zn5E*2FYqUgG_-E$%{K#u+xLY@^xDw>;ldpXOWQX#7$@yKS$06)Z;1({ms05cfF3Kr zm4wps6eYUkX$|-zFFga=(**lnl+vM_c1dHYJk_40GqQH3FhKJ*eIt+1baBC)`97f3ESgO|ad@<=j zop*xG@LQv+Om)oK!g%;{0_@UbeY37s$s9ZRez4eIyt(8=Kx(d5X&+VDo^BYB+Iqbw z{Rdtk#%Z5WrH9xMULqXLL2%0ccHxXR;gyur3zN~8#d{Qichx6a&5et;2U;N&sAgC( zj-am1?gcP}{CGRtn~o6}B?ycl1w;vb!-;%pg}=JMU7d1$gBdFn-3ta3TTvX z3iVo_le`g2y(R%2p83ZWXq?fV8gpL3AMV$JkbSUg=~`4v^($z4V9=9bSnJrQdd9@Bxoe)vau-D8lI0R)Mo7J|J*h|UiBM^YEN3P*stFx@idJbq)6uA0n1 zVl9)K8Of~h;-s?QO7fW00Wm>IHMDU;nNI$POIi9YMOdD1j2{pa(iHW731^IFxH=a8#!=NC61Hxepruw-qq;S2PnqQd zns;vE=;Pd zq@qX^n#yvQNQnea!8neBg2mJL3qfK4m=kqSPzn&|E>3dYJNoDjnLr4G1O^4YKfua< zELWJpxRY6})P10hK4Hg@s-5yc4PD$KpXwAg6Krh#P0p}S8jnZw!k z!8h#x-l@lwbib(h=aO~!NA4H)M>Bg@vMdV-@*&lFao1S&3KWI-lZBm zC*sS)t|-iSe+>!q>cpMa_u=ji0Ws_gid0No zq!pHKxY5{@Ot+4-fORG_TFGrpZDd)s*;MA-@O^y?@P;gU;1N-7dy8{)xv_A~t;s== z-OPWqXiBsA7}vC#W$;Zx4}1r+ir15R%ny=!r=g*9I=3c9k;PO`KLx25k1k#1l>VBk zYSkcJP*;$Z*|s}hH|-4UlVPb=_FRoD(=|x9r|Y8^0{!{rO9Qu9j{_BLP5!}*tg7XJP#(RZZ2z8 zeJHWTAmTN$aGQ~IbD3Kkrw4A0pS=i18K}z5uv({s2wp|W35BuPN!qE*9`WN|ovdck z1s5vLrWJGoCtc_QyYIdv?-*N18;42@ELKcGG8$oviteDsYrMh){28V2AY@6}yCyJ? zM8R(ln)5#^FKLu$TaDv+SBtSC;5(HUlVfk(qK~;DCfZ-gy2ozce!`LBOS@{4M!$X7 zm@%HvhG8Sdf^F9wZQruIOfxSR%Oih|JtiNrSH`1}dAS(Ry(S5xdbMIVSrfTPxn~9r zZN;&M=WyXo=Z+QK=n;jJ8Fr*Gx}TQX6W`35(US7(A)2W72AMWCDhf_*7l8|bu^**C z8ycPqH3y$;2xcr5wIE)KZ4zy(Zbs8Nrq^*UvwJ$fbf zCbyt7Id8Roy|5l&Se9$n=CpHC8=&DV+mp9NA@7@z`W>HfX6m16W(8VwF#O`tp`_QO z#b~2C;N+_?@Y*0X0u4^l5ZQnXUhbHsh+Y|g*Qh^$=qNs*#Z*0%5X`#)9)? zQ$k@Ql#Ih0EZUCPnu@ z{+|hMa>sI*j1T)wq1fmlNr|Vstt#;YrJz)eq-iM&zULT_zpOGAg1#A#>Ri4BxR;{$Tzax6}*H*AM{T7Awt^@xEeE=G-x z=}}oHqX6U^J93+Jy=N8D$s_FX$7|QFqXQ^EDO*daL*$$&t%vuF<*)MiMN{s@H;lwf zQX0eT6`V9j`hLTzJlW_XSkgs5EIHKB$FGjO!j@QF4-Wm~FC7!;0N zZ#Lq6_V4y&9>=ZRj?~BRa@=-c=J>5=tfnX?39aCWQw7n71Y$Rfjz_WC!C#SiA+NN) zA&S}I`?r|l1tjr?6~0i?DSbq;e`bph2C2|hf~-UF8mhGB=)M4;Pe{B^j37@?)MrYn zi-%B)CK*znjyQZlY_b?mviY$sni0%KKr6+_j)eO_TnUas#u+}nHg~6yKuSG5+D^^^%o`BUwU*mK%Sp$^kH51F7As@`awuJr`n$KoJBouqN+oiDdWzElLcI z=t%n!*@jJX4a)_2Kn;~8%z~MD`I6ogwx=KT)=N+hvNLIn9@kH8cs}TleAQo3(+R|O zINcJa!HZl&O?plJstPmOHLEa*w2yQVr->5n9YBnODM~=%kRs(~6^IAN0IOxb>IpSm zqtPUf_9MIM0^_=h_K9eyC%JiZ_kiYw3y~`*kUIpFJBZI63waLrvY^E)CbFh*>iBP4 zP7FPpS zkLcvb>#r&whSOL2TI$T2#5=3-iMjY+mO?ZdM?eBFtp%}dDd>xoCls_?-?Y z6t+`bam8~F0&fu&Q0^0>kMtPbB3&>DA9I^oHUu77u5pB}3EkimbmO2sWAl*iaqz*1 zP-eUMHjAnkN5#HVv_%CtW&8l_4q(hBzq*r1r;;sg>_ol!=YG7d@Q&EI7ToesKx8-sQxxN_^+H_so4j;%p3WOt(QY@czkge8zkToidZF2}|HmTdKs7tfyCfG{VT z8=)}A#rQNNo+63J+!5p8g%7a&`TF@UQ^G`?*{c-GOtS7F9 z+xM(BLGiL?qq=`h{-tKa(XwDCYLZO21$-}eDHkle!ID4pk41jU!*{$wd?9bUKFK;cr0Ubc)O&hB>u6+40-}g-%>(5;b;_RPSj=!3}1C-(&Dwlu$>~CKD z#Us1wVD`oMDckoV=q=y>^sgT5s~SaIIs}Ioc-j~E0(&57c@u;GsoW7*xih%<`wRZ3 zW}htVv>e<2WOVjkAnLo1|0^G{?`}Wc-?sk{``e($Ks{dLs~x}2`Ykp8;!7KVcEd0y z%1K4f_D*6?L6+6dc5ivGJX9Vjey98=(jb{x&h?p1fp5Z3d0(P@N(0G2>5uO;MXK2v z1MqVNF|k&GySG?!SQag@vR3stNm<#S77KTd1{7T!;LRt3?% zN7m9#W6)s1QM~zI^CO?VKRBfo9yf_P7XeJxxZ~T}>l6kpuB_+^m5%#q0~@qvFPY;?0NF)no#k(`(O8wI zA!sya)x=s{EG7tq&3t^4MD}IZ?7#XMWaTW}+IER;e}JmY>&@j_%i&qF{nXEjEVKYj4XU O0pcZ+k>w@918VP06b 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 literal 0 HcmV?d00001 diff --git a/mmt/jug-asl-2.0.0.jar b/mmt/jug-asl-2.0.0.jar new file mode 100644 index 0000000000000000000000000000000000000000..c86a68f97bb7097bc9824807692b8c387aa24348 GIT binary patch literal 31906 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 literal 0 HcmV?d00001 diff --git a/mmt/truezip.jar b/mmt/truezip.jar new file mode 100644 index 0000000000000000000000000000000000000000..fe6cd1ea00b98d2b95cfa522b5af4ac2ec1f0a8d GIT binary patch literal 152386 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�gVn-1eHB)4JA{GiU^y9vQDr|e1IGBIH}({I%} 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

Nv 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 E5A)NCA^-pY literal 0 HcmV?d00001 diff --git a/rm-server/.classpath b/rm-server/.classpath index b829149aab..be9bb7c2f2 100644 --- a/rm-server/.classpath +++ b/rm-server/.classpath @@ -1,281 +1,281 @@ + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + diff --git a/rm-server/build.gradle b/rm-server/build.gradle index 06d1bfe380..970a8be1f7 100644 --- a/rm-server/build.gradle +++ b/rm-server/build.gradle @@ -1,19 +1,30 @@ - -repositories { - mavenCentral() -} - dependencies { - compile fileTree(dir: 'libs', include: '*.jar') - compile 'javax.servlet:servlet-api:2.5' - compile 'org.springframework:spring-test:2.5' + + providedCompile fileTree(dir: 'libs', include: '*.jar') + providedCompile 'javax.servlet:servlet-api:2.5' + providedCompile 'org.springframework:spring-test:2.5' } -amp { - amp 'rm-server.amp' - module 'config/alfresco/module/org_alfresco_module_rm' - jar 'build/libs/rm-server.jar' +tomcatRun.httpPort = 8080 +tomcatRun.stopPort = 8085 +prepTomcat.warName = 'alfresco.war' +war.baseName = 'alfresco' + +jar.baseName = JAR_NAME + +dependantWar.warName = 'alfresco.war' +dependantWar.warFile = file('alfresco.war') + +amp.amp = AMP_NAME +amp.module = 'config/alfresco/module/org_alfresco_module_rm' +amp.jar = "build/libs/${JAR_NAME}.jar" + +installAmp.ampFileLocation = file('build/dist/' + AMP_NAME) +installAmp.warFileLocation = file('build/dist/alfresco.war') + +copyWar { + from 'alfresco.war' + into 'build/dist' } - diff --git a/rm-server/gradle.properties b/rm-server/gradle.properties index 97dce3567d..53a282f595 100644 --- a/rm-server/gradle.properties +++ b/rm-server/gradle.properties @@ -1,2 +1,4 @@ -warFile=war/alfresco.war -warFileName=Alfresco +FILE_WAR=alfresco.war +WAR_BASE_NAME=alfresco +JAR_NAME=alfresco-rm-2.0 +AMP_NAME=alfresco-rm-2.0.amp From da53994d23756487115f7f2f997d6cb93451aa29 Mon Sep 17 00:00:00 2001 From: Roy Wetherall Date: Thu, 29 Mar 2012 09:05:16 +0000 Subject: [PATCH 10/24] RM Build Scripts: * Ripped out Gradle Tomcat package, now deploys to configured tomcats * Tidyed up configuration, all the important stuff is done in the sub-project property files now * testing from deploy all the way down to unpacking the dependancies git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/modules/recordsmanagement/HEAD@34880 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- build.gradle | 178 ++++++++++++++++++------------------ rm-server/build.gradle | 33 +------ rm-server/gradle.properties | 14 ++- 3 files changed, 104 insertions(+), 121 deletions(-) diff --git a/build.gradle b/build.gradle index df1acaf91b..5813cca0d3 100644 --- a/build.gradle +++ b/build.gradle @@ -1,93 +1,91 @@ buildscript { repositories { - add(new org.apache.ivy.plugins.resolver.URLResolver()) { - name = 'GitHub' - addArtifactPattern 'http://cloud.github.com/downloads/[organisation]/[module]/[module]-[revision].[ext]' - } flatDir { dirs 'mmt' } } dependencies { - classpath 'bmuschko:gradle-tomcat-plugin:0.9.1' classpath fileTree(dir: 'mmt', include: '*.jar') } } -/** Wrapper task */ -task wrapper(type: Wrapper) { - gradleVersion = '0.9' -} - /** Subproject configuration */ - subprojects { apply plugin: 'java' - apply plugin: 'eclipse' - apply plugin: 'tomcat' sourceCompatibility = 1.6 targetCompatibility = 1.6 - task dependantWar explodedDepsDir = 'explodedDeps' - dependantWar.explodedDir = file(explodedDepsDir) - dependantWar.explodedLibDir = file("${explodedDepsDir}/lib") - dependantWar.explodedConfigDir = file("${explodedDepsDir}/config") + explodedLibsDir = "${explodedDepsDir}/lib" + buildDistDir = 'build/dist' + buildLibDir = 'build/libs' + sourceJavaDir = 'source/java' + sourceWebDir = 'source/web' + configDir = 'config' + configModuleDir = "config/alfresco/module/${moduleid}" + moduleProperties = 'module.properties' + fileMapping = 'file-mapping.properties' + baseName = "${groupid}-${appName}-${version}" + jarFile = "${baseName}.jar" + ampFile = "${baseName}.amp" sourceSets { main { java { - srcDir 'source/java' + srcDir sourceJavaDir } } } repositories { + flatDir { - dirs dependantWar.explodedLibDir + dirs explodedLibsDir } mavenCentral() } dependencies { - providedCompile fileTree(dir: dependantWar.explodedLibDir, include: '*.jar') - - def tomcatVersion = '6.0.29' - tomcat "org.apache.tomcat:catalina:${tomcatVersion}", - "org.apache.tomcat:coyote:${tomcatVersion}", - "org.apache.tomcat:jasper:${tomcatVersion}", - 'postgresql:postgresql:9.0-801.jdbc4' + compile fileTree(dir: explodedLibsDir, include: '*.jar') } - /* --- Compile tasks */ + /** --- Compile tasks --- */ + // make sure that the dependancies have been unpacked before compiling the Java compileJava.doFirst { explodeDeps.execute() } - /* --- Dependancy tasks --- */ + jar.archiveName = jarFile + /** --- Dependancy tasks --- */ + task explodeDeps << { - if (dependantWar.warFile.exists() == true) { + explodedDir = file(explodedDepsDir) + explodedLibDir = file(explodedLibsDir) + explodedConfigDir = file("${explodedDepsDir}/config") + warFileObj = file(warFile) + + if (warFileObj.exists() == true) { - println "${dependantWar.warName} was found. Checking dependancies ..." + logger.lifecycle "${warFile} was found. Checking dependancies ..." - if (dependantWar.explodedDir.exists() == false) { - println(" ... creating destination dir ${dependantWar.explodedDir}") - dependantWar.explodedDir.mkdir() + if (explodedDir.exists() == false) { + println(" ... creating destination dir ${explodedDir}") + explodedDir.mkdir() } - if (isUnpacked(dependantWar.explodedLibDir) == false) { + if (isUnpacked(explodedLibDir) == false) { - println(" ... unpacking libs into ${dependantWar.explodedLibDir}") + println(" ... unpacking libs into ${explodedLibDir}") - ant.unzip(src: dependantWar.warFile, dest: dependantWar.explodedLibDir) { + ant.unzip(src: warFileObj, dest: explodedLibDir) { ant.patternset { ant.include(name: 'WEB-INF/lib/*.jar') } @@ -95,109 +93,113 @@ subprojects { } } - if (isUnpacked(dependantWar.explodedConfigDir) == false) { + if (isUnpacked(explodedConfigDir) == false) { - println(" ... unpacking config into ${dependantWar.explodedConfigDir}") + println(" ... unpacking config into ${explodedConfigDir}") - ant.unzip(src: dependantWar.warFile, dest: dependantWar.explodedDir) { + ant.unzip(src: warFileObj, dest: explodedDir) { ant.patternset { ant.include(name: 'WEB-INF/classes/**/*') } } copy { - from "${dependantWar.explodedDir}/WEB-INF/classes" - into dependantWar.explodedConfigDir + from "${explodedDir}/WEB-INF/classes" + into explodedConfigDir } // TODO understand why this doesn't delete the folder as expected ant.delete(includeEmptyDirs: 'true') { - ant.fileset(dir: "${dependantWar.explodedDir}/WEB-INF", includes: '**/*') + ant.fileset(dir: "${explodedDir}/WEB-INF", includes: '**/*') } } } else { - println "Dependant WAR file ${dependantWar.warName} can not be found. Please place it in ${dependantWar.warFile.getPath()} to continue." + println "Dependant WAR file ${warName} can not be found. Please place it in ${warFile.getPath()} to continue." } } - + task cleanDeps << { ant.delete(includeEmptyDirs: 'true') { - ant.fileset(dir: dependantWar.explodedDir, includes: '**/*') + ant.fileset(dir: explodedDepsDir, includes: '**/*') } - } + } /** --- AMP tasks --- */ - task copyWar(type: Copy) + task copyWar(type: Copy) { + from warFile + into buildDistDir + } task amp(dependsOn: 'jar') << { - // TODO set the inputs and outputs - - // assemble the AMP file - ant.zip(destfile: dist + '/' + amp, update: 'true') { + def jarFilePath = "${buildLibDir}/${jarFile}" + def jarFileObj = file(jarFilePath) + def configDirObj = file(configDir) + def sourceWebObj = file(sourceWebDir) - ant.zipfileset(file: module + '/' + moduleProperties) - ant.zipfileset(file: module + '/' + fileMapping) + // assemble the AMP file + ant.zip(destfile: "${buildDistDir}/${ampFile}", update: 'true') { + + ant.zipfileset(file: "${configModuleDir}/${moduleProperties}") + ant.zipfileset(file: "${configModuleDir}/${fileMapping}") - if (jar != null) { - ant.zipfileset(file: jar, prefix: jarDest) - } + if (jarFileObj.exists()) { - if (config != null) { - ant.zipfileset(dir: config, prefix: configDest) { + logger.info("Adding ${jarFilePath} to ${ampFile} in /lib") + ant.zipfileset(file: jarFilePath, prefix: 'lib') + } + + if (configDirObj.exists() == true) { + + logger.info("Adding ${configDir} to ${ampFile} in /config") + + ant.zipfileset(dir: configDir, prefix: 'config') { ant.exclude(name: '**/' + moduleProperties) ant.exclude(name: '**/' + fileMapping) } } - if (web != null) { - ant.zipfileset(dir: web, prefix: webDest) + if (sourceWebObj.exists() == true) { + + logger.info("Adding ${sourceWebDir} to ${ampFile} in /web") + + ant.zipfileset(dir: sourceWebDir, prefix: 'web') } } } - amp.dist = 'build/dist' - amp.config = 'config' - amp.moduleProperties = 'module.properties' - amp.fileMapping = 'file-mapping.properties' - amp.jarDest = 'lib' - amp.configDest = 'config' - amp.webDest = 'web' - amp.web = null - task installAmp(dependsOn: ['amp', 'copyWar']) << { + + def warFileLocation = file("${buildDistDir}/${warFile}") + def ampFileLocation = file("${buildDistDir}/${ampFile}") + mmt = new org.alfresco.repo.module.tool.ModuleManagementTool() mmt.setVerbose(true) mmt.installModule(ampFileLocation.getPath(), warFileLocation.getPath(), false, true, false) } - /** --- WAR/Tomcat configuration --- */ - - war.doFirst { - throw new StopExecutionException(); + task cleanDeploy(type: Delete) { + delete "${tomcatRoot}/webapps/${webAppName}", "${tomcatRoot}/webapps/${warFile}" } - war.destinationDir = file('build/tomcat') + task deployAmp(dependsOn: ['cleanDeploy', 'installAmp']) << { - task prepTomcat << { - - copy { - from zipTree("build/dist/${warName}") - into 'build/tomcat' - } - if (warName.equals('alfresco.war') == true) { + println tomcatRoot + tomcatRootDir = new File(tomcatRoot) + if (tomcatRootDir.exists() == true) { + + // copy war copy { - from 'data/dev-context.xml' - into 'build/tomcat/WEB-INF/classes/alfresco/extension' + from "${buildDistDir}/${warFile}" + into "${tomcatRoot}/webapps" } - } + } + else { + println "Tomcat root directory ${tomcatRoot} does not exist." + } } - - tomcatRun.dependsOn prepTomcat - tomcatRun.webAppSourceDirectory = file('build/tomcat') - tomcatRun.uriroot = 'build/tomcat' } /** Utility function - indicates wether the provided dir is unpacked (ie exists and has some contents) */ diff --git a/rm-server/build.gradle b/rm-server/build.gradle index 970a8be1f7..f11203b9a3 100644 --- a/rm-server/build.gradle +++ b/rm-server/build.gradle @@ -1,30 +1,5 @@ dependencies { - - providedCompile fileTree(dir: 'libs', include: '*.jar') - providedCompile 'javax.servlet:servlet-api:2.5' - providedCompile 'org.springframework:spring-test:2.5' -} - -tomcatRun.httpPort = 8080 -tomcatRun.stopPort = 8085 -prepTomcat.warName = 'alfresco.war' -war.baseName = 'alfresco' - -jar.baseName = JAR_NAME - -dependantWar.warName = 'alfresco.war' -dependantWar.warFile = file('alfresco.war') - -amp.amp = AMP_NAME -amp.module = 'config/alfresco/module/org_alfresco_module_rm' -amp.jar = "build/libs/${JAR_NAME}.jar" - -installAmp.ampFileLocation = file('build/dist/' + AMP_NAME) -installAmp.warFileLocation = file('build/dist/alfresco.war') - -copyWar { - from 'alfresco.war' - into 'build/dist' -} - - + compile fileTree(dir: 'libs', include: '*.jar') + compile 'javax.servlet:servlet-api:2.5' + compile 'org.springframework:spring-test:2.5' +} \ No newline at end of file diff --git a/rm-server/gradle.properties b/rm-server/gradle.properties index 53a282f595..565233d0cc 100644 --- a/rm-server/gradle.properties +++ b/rm-server/gradle.properties @@ -1,4 +1,10 @@ -FILE_WAR=alfresco.war -WAR_BASE_NAME=alfresco -JAR_NAME=alfresco-rm-2.0 -AMP_NAME=alfresco-rm-2.0.amp +groupid=alfresco +appName=rm +version=2.0 + +moduleid=org_alfresco_module_rm + +tomcatRoot=C:/mywork/projects/rmhead/software/tomcat + +webAppName=alfresco +warFile=alfresco.war \ No newline at end of file From 286763563de83b88ca600595c1843c3bc60d97b8 Mon Sep 17 00:00:00 2001 From: Roy Wetherall Date: Fri, 30 Mar 2012 03:25:05 +0000 Subject: [PATCH 11/24] RM build scripts: * Error message has incorrect variable references git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/modules/recordsmanagement/HEAD@34907 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 5813cca0d3..2bdbc92de7 100644 --- a/build.gradle +++ b/build.gradle @@ -115,7 +115,7 @@ subprojects { } } else { - println "Dependant WAR file ${warName} can not be found. Please place it in ${warFile.getPath()} to continue." + println "Dependant WAR file ${warFile} can not be found. Please place it in ${warFileObj.getPath()} to continue." } } From 1423bc64cebc0095278aecf2f7a3ba18b8f9a0cc Mon Sep 17 00:00:00 2001 From: Roy Wetherall Date: Fri, 30 Mar 2012 03:38:50 +0000 Subject: [PATCH 12/24] RM build script: * Error handling corrected git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/modules/recordsmanagement/HEAD@34908 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- build.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 2bdbc92de7..9a55a875b4 100644 --- a/build.gradle +++ b/build.gradle @@ -115,7 +115,8 @@ subprojects { } } else { - println "Dependant WAR file ${warFile} can not be found. Please place it in ${warFileObj.getPath()} to continue." + logger.error "Dependant WAR file ${warFile} can not be found. Please place it in ${warFileObj.getPath()} to continue." + throw new TaskInstantiationException("Dependant WAR file ${warFile} can not be found. Please place it in ${warFileObj.getPath()} to continue.") } } From 445c9bb6c32a5d41f80e79f6844f5f2c2d88f882 Mon Sep 17 00:00:00 2001 From: Roy Wetherall Date: Fri, 30 Mar 2012 05:02:40 +0000 Subject: [PATCH 13/24] RM build sciprts: * Missing test dependancy git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/modules/recordsmanagement/HEAD@34909 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- .../libs/spring-webscripts-1.0.0-tests.jar | Bin 0 -> 76498 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 rm-server/libs/spring-webscripts-1.0.0-tests.jar diff --git a/rm-server/libs/spring-webscripts-1.0.0-tests.jar b/rm-server/libs/spring-webscripts-1.0.0-tests.jar new file mode 100644 index 0000000000000000000000000000000000000000..1b386b0743f0f0ba53db717d0d9559f2f7c749e1 GIT binary patch literal 76498 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 Date: Fri, 30 Mar 2012 05:12:49 +0000 Subject: [PATCH 14/24] RM build scripts: * deployAmp tasks configured to use the TOMCAT_HOME and APP_TOMCAT_HOME evnVars for the rm-server and rm-share projects respectively. (so things work easily when using the DevEnv) * ReadMe.txt added with steps to setup and use the RM gradle scripts (will copy this up to the RM 2.0 site on ts as well) git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/modules/recordsmanagement/HEAD@34910 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- ReadMe.txt | 55 +++++++++++++++++++++++++++++++++++++ build.gradle | 2 +- rm-server/gradle.properties | 6 ++-- 3 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 ReadMe.txt diff --git a/ReadMe.txt b/ReadMe.txt new file mode 100644 index 0000000000..5b59245a47 --- /dev/null +++ b/ReadMe.txt @@ -0,0 +1,55 @@ +== Alfresco Records Management - Development Environment Setup == + +Prerequisites + + - Gradle 1.0 milestone 8a (or higher). + - Alfresco Repository 4.0.1 (or higher). Specifically you need the alfresco.war and share.war files. + - Any prerequisties required for an Alfresco installation, including Java 1.5, Tomcat and a suitable database. + +Initial Setup + + - Checkout Records Management code from https://svn.alfresco.com/repos/alfresco-enterprise/modules/recordsmanagement/HEAD + Note: the RM branch structure reflects the main repository branch structure, so HEAD is the current development branch and BRACHES contains the release branches. + - Place the alfresco.war in the root of the rm-server directory and place the share.war in the root of the rm-share directory. + Note: building BRACHES/V4.0 will provide compatiable wars, alternatively a compatible build can be retrieved from bamboo.alfresco.com + - Run "gradle amp" in root directory. This will unpack the dependancies, build the RM source and finally assemble the RM amps. + Note: the first execution of this task may take serveral minutes, because it will unpack the required build dependancies from the alfresco and share WAR. It will + also pull any external dependancies from Maven or + - You will not find rm-server\build\dist\alfresco-rm-2.0.amp and rm-share\build\dist\alfresco-rm-share-2.0.amp ahve been built. + +Using Eclipse + + - Start Eclipse in the usual way. + - Import projects found in rm-server and rm-share directories. + +Deploying the RM AMPs + + - Set the envoronment variables TOMCAT_HOME and APP_TOMCAT_HOME to the home directory of the repository and share Tomcat instances respectively. + NOTE: these can be the same tomcat instance, but it is recommended that two are used. + - Configure your repository Tomcat so that your repository.properties settings will be successfully picked up when Alfresco is started. + - Run "gradle installAmp" in the root directory. This will use the MMT to apply the RM AMPs to the Alfresco and Share WARs respectively. The modified WARs will then + be copied to the set Tomcat instances, cleaning any exisiting exploded WARs. + - Start Tomcat(s). + +For users of the Alfresco DevEnv + + - Create a normal project using "create-project". + - Manually check out RM code into the "code" directory as described above. + - Note that a copy of Gradle is available in the root software directory. + - The devEnv will automatically set the TOMCAT_HOME and APP_TOMCAT_HOME environment variables to point to the Tomcat instances created by the use-tomcat6 and use-app-tomcat6 + scipts. Magic! + - You can use the dev-context.xml generated for you to configure the repository. Place it in /shared/alfresco/extension. + +Summary Of Available Gradle Tasks + + Note: All these tasks can be executed in the root directory or in either of the sub-project directories. + Note: Use the command "gradle " when executing. + Note: The RM Gradle scripts import the standard "Java" package so those associated standard tasks are available, for example "jar", "compileJava", "clean", etc + + - explodeDeps : checks for existance of the projects dependant WAR (either alfresco.war or share.war). If not already exploded, unpacks the required depedancies + from the WAR files. + - cleanDeps : cleans the projects exploded dependancies. + - amp : builds the projects AMP and places it in build/dist. + - installAmp : installs the AMP into a copy of the projects dependant WAR using the MMT. + NOTE: the installed WAR can be found in build/dist. + - deployAmp : depolys the project AMP to the configured Tomcat instance. \ No newline at end of file diff --git a/build.gradle b/build.gradle index 9a55a875b4..e991423f20 100644 --- a/build.gradle +++ b/build.gradle @@ -32,6 +32,7 @@ subprojects { baseName = "${groupid}-${appName}-${version}" jarFile = "${baseName}.jar" ampFile = "${baseName}.amp" + tomcatRoot = System.getenv(tomcatEnv) sourceSets { main { @@ -187,7 +188,6 @@ subprojects { task deployAmp(dependsOn: ['cleanDeploy', 'installAmp']) << { - println tomcatRoot tomcatRootDir = new File(tomcatRoot) if (tomcatRootDir.exists() == true) { diff --git a/rm-server/gradle.properties b/rm-server/gradle.properties index 565233d0cc..258a2334ac 100644 --- a/rm-server/gradle.properties +++ b/rm-server/gradle.properties @@ -4,7 +4,7 @@ version=2.0 moduleid=org_alfresco_module_rm -tomcatRoot=C:/mywork/projects/rmhead/software/tomcat - webAppName=alfresco -warFile=alfresco.war \ No newline at end of file +warFile=alfresco.war + +tomcatEnv=TOMCAT_HOME \ No newline at end of file From 3ce8f10fc992721671133671189469367bfe67b3 Mon Sep 17 00:00:00 2001 From: Roy Wetherall Date: Fri, 30 Mar 2012 07:00:02 +0000 Subject: [PATCH 15/24] RM Bugs: * legacy dod:series class causing issues on a clean install when accessing the custom properties * updated read me git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/modules/recordsmanagement/HEAD@34911 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- ReadMe.txt | 1 + .../RecordsManagementAdminServiceImpl.java | 12 ++++++++++-- .../RecordsManagementTypeFormFilter.java | 19 +++++++++++-------- .../script/CustomPropertyDefinitionsGet.java | 7 +++++-- 4 files changed, 27 insertions(+), 12 deletions(-) diff --git a/ReadMe.txt b/ReadMe.txt index 5b59245a47..139e966e86 100644 --- a/ReadMe.txt +++ b/ReadMe.txt @@ -20,6 +20,7 @@ Initial Setup Using Eclipse - Start Eclipse in the usual way. + Note: make sure the WAR dependancies have been exploded before opening Eclispe. - Import projects found in rm-server and rm-share directories. Deploying the RM AMPs diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/RecordsManagementAdminServiceImpl.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/RecordsManagementAdminServiceImpl.java index 3500f1d704..0b15d38b5d 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/RecordsManagementAdminServiceImpl.java +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/RecordsManagementAdminServiceImpl.java @@ -630,7 +630,11 @@ public class RecordsManagementAdminServiceImpl implements RecordsManagementAdmin Map result = new HashMap(); for (QName customisableType : getCustomisable()) { - result.putAll(getCustomPropertyDefinitions(customisableType)); + Map props = getCustomPropertyDefinitions(customisableType); + if (props != null) + { + result.putAll(props); + } } return result; } @@ -640,9 +644,13 @@ public class RecordsManagementAdminServiceImpl implements RecordsManagementAdmin */ public Map getCustomPropertyDefinitions(QName customisableType) { + Map propDefns = null; QName relevantAspectQName = getCustomAspect(customisableType); AspectDefinition aspectDefn = dictionaryService.getAspect(relevantAspectQName); - Map propDefns = aspectDefn.getProperties(); + if (aspectDefn != null) + { + propDefns = aspectDefn.getProperties(); + } return propDefns; } diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/forms/RecordsManagementTypeFormFilter.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/forms/RecordsManagementTypeFormFilter.java index cf298cfa3f..b586652f4a 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/forms/RecordsManagementTypeFormFilter.java +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/forms/RecordsManagementTypeFormFilter.java @@ -125,16 +125,19 @@ public class RecordsManagementTypeFormFilter extends RecordsManagementFormFilter ParameterCheck.mandatory("form", form); Map customProps = rmAdminService.getCustomPropertyDefinitions(customisableType); - - if (logger.isDebugEnabled() == true) + + if (customProps != null) { - logger.debug("Found " + customProps.size() + " custom properties for customisable type " + customisableType); + if (logger.isDebugEnabled() == true) + { + logger.debug("Found " + customProps.size() + " custom properties for customisable type " + customisableType); + } + + // setup field definition for each custom property + Collection properties = customProps.values(); + List fields = FieldUtils.makePropertyFields(properties, CUSTOM_RM_FIELD_GROUP, namespaceService); + form.addFields(fields); } - - // setup field definition for each custom property - Collection properties = customProps.values(); - List fields = FieldUtils.makePropertyFields(properties, CUSTOM_RM_FIELD_GROUP, namespaceService); - form.addFields(fields); } /* diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomPropertyDefinitionsGet.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomPropertyDefinitionsGet.java index 0a0d06ea69..c01a3542be 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomPropertyDefinitionsGet.java +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/script/CustomPropertyDefinitionsGet.java @@ -96,9 +96,12 @@ public class CustomPropertyDefinitionsGet extends BaseCustomPropertyWebScript { QName customisableType = mapToTypeQName(elementName); Map currentCustomProps = rmAdminService.getCustomPropertyDefinitions(customisableType); - for (Entry entry : currentCustomProps.entrySet()) + if (currentCustomProps != null) { - propData.add(entry.getValue()); + for (Entry entry : currentCustomProps.entrySet()) + { + propData.add(entry.getValue()); + } } } else From b0180e599ac59e51afb428c27d771bbeb89ddd81 Mon Sep 17 00:00:00 2001 From: Roy Wetherall Date: Wed, 4 Apr 2012 02:47:07 +0000 Subject: [PATCH 16/24] RM Build Scripts: * generated gradle wrapper scripts for windows and linux * build tasks can now be called using 'gradlew' and any required Gradle dependancies will automatically be downloaded. Gradle is not required to be installed. * hopefully 'gradlew' can be used by Bamboo as a command task, as it doesn't have native Gradle support installed * updated ReadMe.txt with new information git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/modules/recordsmanagement/HEAD@35068 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- ReadMe.txt | 21 ++- build.gradle | 4 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 45332 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 164 +++++++++++++++++++++++ gradlew.bat | 90 +++++++++++++ 6 files changed, 273 insertions(+), 12 deletions(-) create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat diff --git a/ReadMe.txt b/ReadMe.txt index 139e966e86..4fb0037ec4 100644 --- a/ReadMe.txt +++ b/ReadMe.txt @@ -2,19 +2,20 @@ Prerequisites - - Gradle 1.0 milestone 8a (or higher). + - Gradle 1.0 milestone 8a (optional unless native gradle support is required) - Alfresco Repository 4.0.1 (or higher). Specifically you need the alfresco.war and share.war files. - Any prerequisties required for an Alfresco installation, including Java 1.5, Tomcat and a suitable database. + NOTE: throughout these instructions we will use the 'gradlew' wrapper command. This wrapper command downloads the required Gradle libraries automatically. Should you prefer to use the native 'gradle' command instead you will need to install Gradle manually. + Initial Setup - Checkout Records Management code from https://svn.alfresco.com/repos/alfresco-enterprise/modules/recordsmanagement/HEAD Note: the RM branch structure reflects the main repository branch structure, so HEAD is the current development branch and BRACHES contains the release branches. - Place the alfresco.war in the root of the rm-server directory and place the share.war in the root of the rm-share directory. Note: building BRACHES/V4.0 will provide compatiable wars, alternatively a compatible build can be retrieved from bamboo.alfresco.com - - Run "gradle amp" in root directory. This will unpack the dependancies, build the RM source and finally assemble the RM amps. - Note: the first execution of this task may take serveral minutes, because it will unpack the required build dependancies from the alfresco and share WAR. It will - also pull any external dependancies from Maven or + - Run "gradlew amp" in root directory. This will unpack the dependancies, build the RM source and finally assemble the RM amps. + Note: the first execution of this task may take serveral minutes, because it will unpack the required build dependancies from the alfresco and share WAR. It will also pull any external dependancies from Maven or - You will not find rm-server\build\dist\alfresco-rm-2.0.amp and rm-share\build\dist\alfresco-rm-share-2.0.amp ahve been built. Using Eclipse @@ -28,17 +29,14 @@ Deploying the RM AMPs - Set the envoronment variables TOMCAT_HOME and APP_TOMCAT_HOME to the home directory of the repository and share Tomcat instances respectively. NOTE: these can be the same tomcat instance, but it is recommended that two are used. - Configure your repository Tomcat so that your repository.properties settings will be successfully picked up when Alfresco is started. - - Run "gradle installAmp" in the root directory. This will use the MMT to apply the RM AMPs to the Alfresco and Share WARs respectively. The modified WARs will then - be copied to the set Tomcat instances, cleaning any exisiting exploded WARs. + - Run "gradlew installAmp" in the root directory. This will use the MMT to apply the RM AMPs to the Alfresco and Share WARs respectively. The modified WARs will then be copied to the set Tomcat instances, cleaning any exisiting exploded WARs. - Start Tomcat(s). For users of the Alfresco DevEnv - Create a normal project using "create-project". - - Manually check out RM code into the "code" directory as described above. - - Note that a copy of Gradle is available in the root software directory. - - The devEnv will automatically set the TOMCAT_HOME and APP_TOMCAT_HOME environment variables to point to the Tomcat instances created by the use-tomcat6 and use-app-tomcat6 - scipts. Magic! + - Manually check out RM code into the "code" directory as described above. Don't use the checkout script provided. + - The devEnv will automatically set the TOMCAT_HOME and APP_TOMCAT_HOME environment variables to point to the Tomcat instances created by the use-tomcat6 and use-app-tomcat6 scipts. Magic! - You can use the dev-context.xml generated for you to configure the repository. Place it in /shared/alfresco/extension. Summary Of Available Gradle Tasks @@ -47,8 +45,7 @@ Summary Of Available Gradle Tasks Note: Use the command "gradle " when executing. Note: The RM Gradle scripts import the standard "Java" package so those associated standard tasks are available, for example "jar", "compileJava", "clean", etc - - explodeDeps : checks for existance of the projects dependant WAR (either alfresco.war or share.war). If not already exploded, unpacks the required depedancies - from the WAR files. + - explodeDeps : checks for existance of the projects dependant WAR (either alfresco.war or share.war). If not already exploded, unpacks the required depedancies from the WAR files. - cleanDeps : cleans the projects exploded dependancies. - amp : builds the projects AMP and places it in build/dist. - installAmp : installs the AMP into a copy of the projects dependant WAR using the MMT. diff --git a/build.gradle b/build.gradle index e991423f20..8b43ad0572 100644 --- a/build.gradle +++ b/build.gradle @@ -11,6 +11,10 @@ buildscript { } } +task wrapper(type: Wrapper) { + gradleVersion = '1.0-milestone-8' +} + /** Subproject configuration */ subprojects { diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..5437731f7edebd8ce962fd1bdb09ce776087a394 GIT binary patch literal 45332 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 literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..897e71b9e1 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Apr 04 12:27:15 EST 2012 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=http\://services.gradle.org/distributions/gradle-1.0-milestone-8-bin.zip diff --git a/gradlew b/gradlew new file mode 100644 index 0000000000..ae91ed9029 --- /dev/null +++ b/gradlew @@ -0,0 +1,164 @@ +#!/bin/bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" +APP_HOME="`pwd -P`" +cd "$SAVED" + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query businessSystem maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + JAVA_OPTS="$JAVA_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000000..aec99730b4 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega From f70eb812bcf791065e12eca7799642a1fb8ec4d6 Mon Sep 17 00:00:00 2001 From: Roy Wetherall Date: Thu, 5 Apr 2012 03:01:22 +0000 Subject: [PATCH 17/24] RM Build Scripts and Tests: * moved RM tests into separate folder structure * updated Gradle scripts with test source locations and dependancies .. 'gradlew test' will now attempt to execute the unit tests (even if they fail!) * eclipse project dependancies updated so unit tests execute within the RM eclipse project * TODO get the unit tests working reliabily! (still lots of refactoring of old tests to do) git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/modules/recordsmanagement/HEAD@35093 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- build.gradle | 21 ++++++++++++++++ rm-server/.classpath | 3 +++ rm-server/build.gradle | 23 +++++++++++++++++- rm-server/libs/postgresql-9.0-801.jdbc4.jar | Bin 0 -> 539705 bytes .../test/CapabilitiesTestSuite.java | 0 .../test/DOD5015Test.java | 0 .../test/JScriptTestSuite.java | 0 .../test/ServicesTestSuite.java | 0 .../test/WebScriptTestSuite.java | 0 .../AddModifyEventDatesCapabilityTest.java | 0 ...veRecordsScheduledForCutoffCapability.java | 0 .../capabilities/BaseCapabilitiesTest.java | 0 .../capabilities/BaseTestCapabilities.java | 0 .../test/capabilities/CapabilitiesTest.java | 0 .../DeclarativeCapabilityTest.java | 0 .../test/jscript/CapabilitiesTest.js | 0 .../jscript/JSONConversionComponentTest.java | 0 .../test/jscript/RMJScriptTest.java | 0 .../service/DispositionServiceImplTest.java | 0 .../RMCaveatConfigServiceImplTest.java | 0 ...ecordsManagementActionServiceImplTest.java | 2 +- ...RecordsManagementAdminServiceImplTest.java | 0 ...RecordsManagementAuditServiceImplTest.java | 0 ...RecordsManagementEventServiceImplTest.java | 0 ...ecordsManagementSearchServiceImplTest.java | 0 ...ordsManagementSecurityServiceImplTest.java | 0 .../RecordsManagementServiceImplTest.java | 0 .../service/VitalRecordServiceImplTest.java | 0 .../test/system/CapabilitiesSystemTest.java | 0 .../test/system/DODDataLoadSystemTest.java | 0 .../NotificationServiceHelperSystemTest.java | 0 .../system/PerformanceDataLoadSystemTest.java | 0 ...ecordsManagementServiceImplSystemTest.java | 0 .../test/util/BaseRMTestCase.java | 2 +- .../test/util/TestAction.java | 0 .../test/util/TestAction2.java | 0 .../test/util/TestActionParams.java | 0 .../test/util/TestUtilities.java | 2 +- .../test/util/TestWebScriptRepoServer.java | 0 .../BootstraptestDataRestApiTest.java | 0 .../webscript/DispositionRestApiTest.java | 0 .../test/webscript/EmailMapScriptTest.java | 0 .../test/webscript/EventRestApiTest.java | 0 .../webscript/RMCaveatConfigScriptTest.java | 0 .../webscript/RMConstraintScriptTest.java | 0 .../test/webscript/RmRestApiTest.java | 2 +- .../test/webscript/RoleRestApiTest.java | 0 .../util => test/resources}/test-context.xml | 2 +- .../util => test/resources}/test-model.xml | 0 .../resources}/testCaveatConfig1.json | 0 .../resources}/testCaveatConfig2.json | 0 51 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 rm-server/libs/postgresql-9.0-801.jdbc4.jar rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/CapabilitiesTestSuite.java (100%) rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/DOD5015Test.java (100%) rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/JScriptTestSuite.java (100%) rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/ServicesTestSuite.java (100%) rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/WebScriptTestSuite.java (100%) rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/AddModifyEventDatesCapabilityTest.java (100%) rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/ApproveRecordsScheduledForCutoffCapability.java (100%) rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/BaseCapabilitiesTest.java (100%) rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/BaseTestCapabilities.java (100%) rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/CapabilitiesTest.java (100%) rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/DeclarativeCapabilityTest.java (100%) rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/jscript/CapabilitiesTest.js (100%) rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/jscript/JSONConversionComponentTest.java (100%) rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/jscript/RMJScriptTest.java (100%) rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/service/DispositionServiceImplTest.java (100%) rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/service/RMCaveatConfigServiceImplTest.java (100%) rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementActionServiceImplTest.java (96%) rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementAdminServiceImplTest.java (100%) rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementAuditServiceImplTest.java (100%) rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementEventServiceImplTest.java (100%) rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementSearchServiceImplTest.java (100%) rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementSecurityServiceImplTest.java (100%) rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementServiceImplTest.java (100%) rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/service/VitalRecordServiceImplTest.java (100%) rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/system/CapabilitiesSystemTest.java (100%) rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/system/DODDataLoadSystemTest.java (100%) rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/system/NotificationServiceHelperSystemTest.java (100%) rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/system/PerformanceDataLoadSystemTest.java (100%) rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/system/RecordsManagementServiceImplSystemTest.java (100%) rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/util/BaseRMTestCase.java (97%) rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestAction.java (100%) rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestAction2.java (100%) rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestActionParams.java (100%) rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestUtilities.java (96%) rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestWebScriptRepoServer.java (100%) rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/BootstraptestDataRestApiTest.java (100%) rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/DispositionRestApiTest.java (100%) rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/EmailMapScriptTest.java (100%) rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/EventRestApiTest.java (100%) rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RMCaveatConfigScriptTest.java (100%) rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RMConstraintScriptTest.java (100%) rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RmRestApiTest.java (97%) rename rm-server/{source => test}/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RoleRestApiTest.java (100%) rename rm-server/{source/java/org/alfresco/module/org_alfresco_module_rm/test/util => test/resources}/test-context.xml (95%) rename rm-server/{source/java/org/alfresco/module/org_alfresco_module_rm/test/util => test/resources}/test-model.xml (100%) rename rm-server/{test-resources => test/resources}/testCaveatConfig1.json (100%) rename rm-server/{test-resources => test/resources}/testCaveatConfig2.json (100%) diff --git a/build.gradle b/build.gradle index 8b43ad0572..4faaac8596 100644 --- a/build.gradle +++ b/build.gradle @@ -15,6 +15,16 @@ task wrapper(type: Wrapper) { gradleVersion = '1.0-milestone-8' } +task downloadAlfresco << { + + def address = "https://bamboo.alfresco.com/bamboo/artifact/ALF-ENTERPRISEV40/JOB1/build-891/ALL/alfresco-enterprise-4.0.1.zip?os_authType=basic&os_username=rwetherall&os_password=31vegaleg" + + def file = new FileOutputStream(file('alfresco.zip')) + def out = new BufferedOutputStream(file) + out << new URL(address).openStream() + out.close() +} + /** Subproject configuration */ subprojects { @@ -25,10 +35,13 @@ subprojects { explodedDepsDir = 'explodedDeps' explodedLibsDir = "${explodedDepsDir}/lib" + explodedConfigDir = "${explodedDepsDir}/config" buildDistDir = 'build/dist' buildLibDir = 'build/libs' sourceJavaDir = 'source/java' sourceWebDir = 'source/web' + testJavaDir = 'test/java' + testResourceDir = 'test/resource' configDir = 'config' configModuleDir = "config/alfresco/module/${moduleid}" moduleProperties = 'module.properties' @@ -42,6 +55,14 @@ subprojects { main { java { srcDir sourceJavaDir + } + } + test { + java { + srcDir testJavaDir + } + resources { + srcDir testResourceDir } } } diff --git a/rm-server/.classpath b/rm-server/.classpath index be9bb7c2f2..dce9320965 100644 --- a/rm-server/.classpath +++ b/rm-server/.classpath @@ -1,6 +1,8 @@ + + @@ -277,5 +279,6 @@ + diff --git a/rm-server/build.gradle b/rm-server/build.gradle index f11203b9a3..708db68c1d 100644 --- a/rm-server/build.gradle +++ b/rm-server/build.gradle @@ -1,5 +1,26 @@ dependencies { + compile fileTree(dir: 'libs', include: '*.jar') compile 'javax.servlet:servlet-api:2.5' - compile 'org.springframework:spring-test:2.5' + + testCompile 'org.springframework:spring-test:2.5' + + testRuntime files(explodedConfigDir) + testRuntime files(configDir) +} + +test { + //makes the standard streams (err and out) visible at console when running tests + testLogging.showStandardStreams = true + + //tweaking memory settings for the forked vm that runs tests + jvmArgs '-Xms256M', '-Xmx1024M', '-XX:MaxPermSize=256M' + + //listening to test execution events + beforeTest { descriptor -> + logger.lifecycle("Running test: " + descriptor) + } + onOutput { descriptor, event -> + logger.lifecycle(event.message) + } } \ No newline at end of file diff --git a/rm-server/libs/postgresql-9.0-801.jdbc4.jar b/rm-server/libs/postgresql-9.0-801.jdbc4.jar new file mode 100644 index 0000000000000000000000000000000000000000..63e54165ecf088a46346ddaaf70ce125b613ff17 GIT binary patch literal 539705 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⩔=a_Z-pZ{bJ~0}K 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

}E+jt#4&8?*CiQSc}yVE}^fvZS@d2V&H^9!H! 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�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%0D{^C>w+$ue87zwR{H5lU=pn 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

^K5FbLbdRLb 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 Nv_Sjk3#b+#{{utge(C@K literal 0 HcmV?d00001 diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/CapabilitiesTestSuite.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/CapabilitiesTestSuite.java similarity index 100% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/CapabilitiesTestSuite.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/CapabilitiesTestSuite.java diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/DOD5015Test.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/DOD5015Test.java similarity index 100% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/DOD5015Test.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/DOD5015Test.java diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/JScriptTestSuite.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/JScriptTestSuite.java similarity index 100% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/JScriptTestSuite.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/JScriptTestSuite.java diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/ServicesTestSuite.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/ServicesTestSuite.java similarity index 100% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/ServicesTestSuite.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/ServicesTestSuite.java diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/WebScriptTestSuite.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/WebScriptTestSuite.java similarity index 100% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/WebScriptTestSuite.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/WebScriptTestSuite.java diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/AddModifyEventDatesCapabilityTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/AddModifyEventDatesCapabilityTest.java similarity index 100% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/AddModifyEventDatesCapabilityTest.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/AddModifyEventDatesCapabilityTest.java diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/ApproveRecordsScheduledForCutoffCapability.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/ApproveRecordsScheduledForCutoffCapability.java similarity index 100% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/ApproveRecordsScheduledForCutoffCapability.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/ApproveRecordsScheduledForCutoffCapability.java diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/BaseCapabilitiesTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/BaseCapabilitiesTest.java similarity index 100% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/BaseCapabilitiesTest.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/BaseCapabilitiesTest.java diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/BaseTestCapabilities.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/BaseTestCapabilities.java similarity index 100% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/BaseTestCapabilities.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/BaseTestCapabilities.java diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/CapabilitiesTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/CapabilitiesTest.java similarity index 100% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/CapabilitiesTest.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/CapabilitiesTest.java diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/DeclarativeCapabilityTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/DeclarativeCapabilityTest.java similarity index 100% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/DeclarativeCapabilityTest.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/DeclarativeCapabilityTest.java diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/jscript/CapabilitiesTest.js b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/jscript/CapabilitiesTest.js similarity index 100% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/jscript/CapabilitiesTest.js rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/jscript/CapabilitiesTest.js diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/jscript/JSONConversionComponentTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/jscript/JSONConversionComponentTest.java similarity index 100% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/jscript/JSONConversionComponentTest.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/jscript/JSONConversionComponentTest.java diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/jscript/RMJScriptTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/jscript/RMJScriptTest.java similarity index 100% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/jscript/RMJScriptTest.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/jscript/RMJScriptTest.java diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/DispositionServiceImplTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/service/DispositionServiceImplTest.java similarity index 100% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/DispositionServiceImplTest.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/service/DispositionServiceImplTest.java diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RMCaveatConfigServiceImplTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/service/RMCaveatConfigServiceImplTest.java similarity index 100% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RMCaveatConfigServiceImplTest.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/service/RMCaveatConfigServiceImplTest.java diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementActionServiceImplTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementActionServiceImplTest.java similarity index 96% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementActionServiceImplTest.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementActionServiceImplTest.java index aaef219d15..8a058abbc3 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementActionServiceImplTest.java +++ b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementActionServiceImplTest.java @@ -63,7 +63,7 @@ public class RecordsManagementActionServiceImplTest extends TestCase { private static final String[] CONFIG_LOCATIONS = new String[] { "classpath:alfresco/application-context.xml", - "classpath:org/alfresco/module/org_alfresco_module_rm/test/util/test-context.xml"}; + "classpath:test-context.xml"}; private ApplicationContext ctx; diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementAdminServiceImplTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementAdminServiceImplTest.java similarity index 100% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementAdminServiceImplTest.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementAdminServiceImplTest.java diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementAuditServiceImplTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementAuditServiceImplTest.java similarity index 100% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementAuditServiceImplTest.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementAuditServiceImplTest.java diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementEventServiceImplTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementEventServiceImplTest.java similarity index 100% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementEventServiceImplTest.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementEventServiceImplTest.java diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementSearchServiceImplTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementSearchServiceImplTest.java similarity index 100% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementSearchServiceImplTest.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementSearchServiceImplTest.java diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementSecurityServiceImplTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementSecurityServiceImplTest.java similarity index 100% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementSecurityServiceImplTest.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementSecurityServiceImplTest.java diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementServiceImplTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementServiceImplTest.java similarity index 100% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementServiceImplTest.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementServiceImplTest.java diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/VitalRecordServiceImplTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/service/VitalRecordServiceImplTest.java similarity index 100% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/service/VitalRecordServiceImplTest.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/service/VitalRecordServiceImplTest.java diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/system/CapabilitiesSystemTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/system/CapabilitiesSystemTest.java similarity index 100% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/system/CapabilitiesSystemTest.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/system/CapabilitiesSystemTest.java diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/system/DODDataLoadSystemTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/system/DODDataLoadSystemTest.java similarity index 100% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/system/DODDataLoadSystemTest.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/system/DODDataLoadSystemTest.java diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/system/NotificationServiceHelperSystemTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/system/NotificationServiceHelperSystemTest.java similarity index 100% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/system/NotificationServiceHelperSystemTest.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/system/NotificationServiceHelperSystemTest.java diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/system/PerformanceDataLoadSystemTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/system/PerformanceDataLoadSystemTest.java similarity index 100% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/system/PerformanceDataLoadSystemTest.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/system/PerformanceDataLoadSystemTest.java diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/system/RecordsManagementServiceImplSystemTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/system/RecordsManagementServiceImplSystemTest.java similarity index 100% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/system/RecordsManagementServiceImplSystemTest.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/system/RecordsManagementServiceImplSystemTest.java diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/BaseRMTestCase.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/util/BaseRMTestCase.java similarity index 97% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/BaseRMTestCase.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/util/BaseRMTestCase.java index 9037eca3fc..131302ffd8 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/BaseRMTestCase.java +++ b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/util/BaseRMTestCase.java @@ -77,7 +77,7 @@ public abstract class BaseRMTestCase extends RetryingTransactionHelperTestCase protected static final String[] CONFIG_LOCATIONS = new String[] { "classpath:alfresco/application-context.xml", - "classpath:org/alfresco/module/org_alfresco_module_rm/test/util/test-context.xml" + "classpath:test-context.xml" }; protected ApplicationContext applicationContext; diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestAction.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestAction.java similarity index 100% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestAction.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestAction.java diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestAction2.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestAction2.java similarity index 100% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestAction2.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestAction2.java diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestActionParams.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestActionParams.java similarity index 100% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestActionParams.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestActionParams.java diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestUtilities.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestUtilities.java similarity index 96% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestUtilities.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestUtilities.java index b579549521..5a1f68f198 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestUtilities.java +++ b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestUtilities.java @@ -102,7 +102,7 @@ public class TestUtilities implements RecordsManagementModel // Do the data load into the the provided filePlan node reference // TODO ... InputStream is = TestUtilities.class.getClassLoader().getResourceAsStream( - "alfresco/module/org_alfresco_module_rm/bootstrap/DODExampleFilePlan.xml"); + "alfresco/module/org_alfresco_module_rm/dod5015/DODExampleFilePlan.xml"); //"alfresco/module/org_alfresco_module_rm/bootstrap/temp.xml"); Assert.assertNotNull("The DODExampleFilePlan.xml import file could not be found", is); Reader viewReader = new InputStreamReader(is); diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestWebScriptRepoServer.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestWebScriptRepoServer.java similarity index 100% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestWebScriptRepoServer.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestWebScriptRepoServer.java diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/BootstraptestDataRestApiTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/BootstraptestDataRestApiTest.java similarity index 100% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/BootstraptestDataRestApiTest.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/BootstraptestDataRestApiTest.java diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/DispositionRestApiTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/DispositionRestApiTest.java similarity index 100% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/DispositionRestApiTest.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/DispositionRestApiTest.java diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/EmailMapScriptTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/EmailMapScriptTest.java similarity index 100% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/EmailMapScriptTest.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/EmailMapScriptTest.java diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/EventRestApiTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/EventRestApiTest.java similarity index 100% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/EventRestApiTest.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/EventRestApiTest.java diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RMCaveatConfigScriptTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RMCaveatConfigScriptTest.java similarity index 100% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RMCaveatConfigScriptTest.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RMCaveatConfigScriptTest.java diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RMConstraintScriptTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RMConstraintScriptTest.java similarity index 100% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RMConstraintScriptTest.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RMConstraintScriptTest.java diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RmRestApiTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RmRestApiTest.java similarity index 97% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RmRestApiTest.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RmRestApiTest.java index 8c3e0e27f6..ed44c7db6e 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RmRestApiTest.java +++ b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RmRestApiTest.java @@ -117,7 +117,7 @@ public class RmRestApiTest extends BaseWebScriptTest implements RecordsManagemen @Override protected void setUp() throws Exception { - setCustomContext("classpath:org/alfresco/module/org_alfresco_module_rm/test/util/test-context.xml"); + setCustomContext("classpath:test-context.xml"); super.setUp(); this.namespaceService = (NamespaceService) getServer().getApplicationContext().getBean("NamespaceService"); diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RoleRestApiTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RoleRestApiTest.java similarity index 100% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RoleRestApiTest.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RoleRestApiTest.java diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/test-context.xml b/rm-server/test/resources/test-context.xml similarity index 95% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/test-context.xml rename to rm-server/test/resources/test-context.xml index 6e094dcc19..56abe6dc78 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/test-context.xml +++ b/rm-server/test/resources/test-context.xml @@ -7,7 +7,7 @@ - org/alfresco/module/org_alfresco_module_rm/test/util/test-model.xml + test-model.xml diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/test-model.xml b/rm-server/test/resources/test-model.xml similarity index 100% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/test/util/test-model.xml rename to rm-server/test/resources/test-model.xml diff --git a/rm-server/test-resources/testCaveatConfig1.json b/rm-server/test/resources/testCaveatConfig1.json similarity index 100% rename from rm-server/test-resources/testCaveatConfig1.json rename to rm-server/test/resources/testCaveatConfig1.json diff --git a/rm-server/test-resources/testCaveatConfig2.json b/rm-server/test/resources/testCaveatConfig2.json similarity index 100% rename from rm-server/test-resources/testCaveatConfig2.json rename to rm-server/test/resources/testCaveatConfig2.json From e767ce187a0fa8fb262da5de3bb13312a06cf03f Mon Sep 17 00:00:00 2001 From: Roy Wetherall Date: Thu, 12 Apr 2012 07:20:20 +0000 Subject: [PATCH 18/24] RM Unit Tests: * Removed web script tests dependancy on DOD test data * refactor base RM test and placed common utils in common class * base rm web script class created * unit tests refactored (still some work on the older tests required) * junit conflict resolved RM Bug Fixs: * user rights view working again * fix from Erik so RM admin console works git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/modules/recordsmanagement/HEAD@35190 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- rm-server/.classpath | 5 +- rm-server/build.gradle | 6 +- .../FileableCapabilityCondition.java | 3 +- ...OD5015Test.java => DOD5015SystemTest.java} | 56 +- .../capabilities/BaseCapabilitiesTest.java | 922 ------------------ .../test/capabilities/CapabilitiesTest.java | 38 +- .../DeclarativeCapabilityTest.java | 26 +- .../jscript/JSONConversionComponentTest.java | 2 +- .../test/jscript/RMJScriptTest.java | 15 +- .../service/DispositionServiceImplTest.java | 35 +- ...RecordsManagementAdminServiceImplTest.java | 16 +- ...ecordsManagementSearchServiceImplTest.java | 12 +- .../RecordsManagementServiceImplTest.java | 2 +- .../service/VitalRecordServiceImplTest.java | 10 +- .../test/system/DODDataLoadSystemTest.java | 78 -- .../NotificationServiceHelperSystemTest.java | 6 +- ...ecordsManagementServiceImplSystemTest.java | 16 +- .../test/util/BaseRMTestCase.java | 164 +--- .../test/util/BaseRMWebScriptTestCase.java | 236 +++++ .../test/util/CommonRMTestUtils.java | 202 ++++ .../test/util/TestUtilities.java | 123 +-- .../BootstraptestDataRestApiTest.java | 3 +- .../webscript/DispositionRestApiTest.java | 251 +---- .../test/webscript/EmailMapScriptTest.java | 4 +- .../test/webscript/EventRestApiTest.java | 58 +- .../webscript/RMCaveatConfigScriptTest.java | 25 +- .../webscript/RMConstraintScriptTest.java | 43 +- .../test/webscript/RmRestApiTest.java | 527 ++-------- .../test/webscript/RoleRestApiTest.java | 74 +- 29 files changed, 784 insertions(+), 2174 deletions(-) rename rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/{DOD5015Test.java => DOD5015SystemTest.java} (97%) delete mode 100644 rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/BaseCapabilitiesTest.java delete mode 100644 rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/system/DODDataLoadSystemTest.java create mode 100644 rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/util/BaseRMWebScriptTestCase.java create mode 100644 rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/util/CommonRMTestUtils.java diff --git a/rm-server/.classpath b/rm-server/.classpath index dce9320965..10f3e73b1d 100644 --- a/rm-server/.classpath +++ b/rm-server/.classpath @@ -9,7 +9,6 @@ - @@ -26,8 +25,8 @@ - - + + diff --git a/rm-server/build.gradle b/rm-server/build.gradle index 708db68c1d..75520e3ae5 100644 --- a/rm-server/build.gradle +++ b/rm-server/build.gradle @@ -20,7 +20,7 @@ test { beforeTest { descriptor -> logger.lifecycle("Running test: " + descriptor) } - onOutput { descriptor, event -> - logger.lifecycle(event.message) - } + //onOutput { descriptor, event -> + // logger.lifecycle(event.message) + //} } \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/FileableCapabilityCondition.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/FileableCapabilityCondition.java index 1b52f2d838..942d73c8eb 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/FileableCapabilityCondition.java +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/FileableCapabilityCondition.java @@ -49,7 +49,8 @@ public class FileableCapabilityCondition extends AbstractCapabilityCondition @Override public boolean evaluate(NodeRef nodeRef) { - QName type = nodeService.getType(nodeRef); + QName type = nodeService.getType(nodeRef); + // TODO and not already a record? return (dictionaryService.isSubClass(type, ContentModel.TYPE_CONTENT) || dictionaryService.isSubClass(type, TYPE_NON_ELECTRONIC_DOCUMENT)); } diff --git a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/DOD5015Test.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/DOD5015SystemTest.java similarity index 97% rename from rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/DOD5015Test.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/DOD5015SystemTest.java index d32ee96e76..0c406b31e0 100644 --- a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/DOD5015Test.java +++ b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/DOD5015SystemTest.java @@ -100,7 +100,7 @@ import org.alfresco.util.PropertyMap; * * @author Roy Wetherall, Neil McErlean */ -public class DOD5015Test extends BaseSpringTest implements RecordsManagementModel, DOD5015Model +public class DOD5015SystemTest extends BaseSpringTest implements RecordsManagementModel, DOD5015Model { private static final Period weeklyReview = new Period("week|1"); private static final Period dailyReview = new Period("day|1"); @@ -222,7 +222,7 @@ public class DOD5015Test extends BaseSpringTest implements RecordsManagementMode public void xtestTestData() throws Exception { // make sure the folders that should have disposition schedules do so - NodeRef janAuditRecordsFolder = TestUtilities.getRecordFolder(searchService, "Reports", "AIS Audit Records", "January AIS Audit Records"); + NodeRef janAuditRecordsFolder = TestUtilities.getRecordFolder(rmService, nodeService, "Reports", "AIS Audit Records", "January AIS Audit Records"); assertNotNull(janAuditRecordsFolder); // ensure the folder has the disposition lifecycle aspect @@ -233,7 +233,7 @@ public class DOD5015Test extends BaseSpringTest implements RecordsManagementMode checkSearchAspect(janAuditRecordsFolder); // check another folder that has events as part of the disposition schedule - NodeRef equalOppCoordFolder = TestUtilities.getRecordFolder(searchService, "Military Files", "Personnel Security Program Records", "Equal Opportunity Coordinator"); + NodeRef equalOppCoordFolder = TestUtilities.getRecordFolder(rmService, nodeService, "Military Files", "Personnel Security Program Records", "Equal Opportunity Coordinator"); assertNotNull(equalOppCoordFolder); assertTrue("Expected 'Equal Opportunity Coordinator' folder to have disposition lifecycle aspect applied", nodeService.hasAspect(equalOppCoordFolder, ASPECT_DISPOSITION_LIFECYCLE)); @@ -260,7 +260,7 @@ public class DOD5015Test extends BaseSpringTest implements RecordsManagementMode public NodeRef execute() throws Throwable { // Create a record folder under a "non-vital" category - NodeRef nonVitalRecordCategory = TestUtilities.getRecordCategory(searchService, "Reports", "Unit Manning Documents"); + NodeRef nonVitalRecordCategory = TestUtilities.getRecordCategory(rmService, nodeService, "Reports", "Unit Manning Documents"); assertNotNull(nonVitalRecordCategory); return createRecFolderNode(nonVitalRecordCategory); @@ -314,7 +314,7 @@ public class DOD5015Test extends BaseSpringTest implements RecordsManagementMode // Create another folder with different vital/disposition instructions //TODO Change disposition instructions - NodeRef vitalRecordCategory = TestUtilities.getRecordCategory(searchService, "Reports", "AIS Audit Records"); + NodeRef vitalRecordCategory = TestUtilities.getRecordCategory(rmService, nodeService, "Reports", "AIS Audit Records"); assertNotNull(vitalRecordCategory); return createRecFolderNode(vitalRecordCategory); } @@ -494,7 +494,7 @@ public class DOD5015Test extends BaseSpringTest implements RecordsManagementMode public void testDispositionLifecycle_0318_01_basictest() throws Exception { - final NodeRef recordCategory = TestUtilities.getRecordCategory(searchService, "Reports", "AIS Audit Records"); + final NodeRef recordCategory = TestUtilities.getRecordCategory(rmService, nodeService, "Reports", "AIS Audit Records"); setComplete(); endTransaction(); @@ -765,7 +765,7 @@ public class DOD5015Test extends BaseSpringTest implements RecordsManagementMode */ public void testDispositionLifecycle_0318_reschedule_folderlevel() throws Exception { - final NodeRef recordSeries = TestUtilities.getRecordSeries(searchService, "Reports"); + final NodeRef recordSeries = TestUtilities.getRecordSeries(rmService, nodeService, "Reports"); setComplete(); endTransaction(); @@ -1076,7 +1076,7 @@ public class DOD5015Test extends BaseSpringTest implements RecordsManagementMode */ public void testDispositionLifecycle_0318_reschedule_recordlevel() throws Exception { - final NodeRef recordSeries = TestUtilities.getRecordSeries(searchService, "Reports"); + final NodeRef recordSeries = TestUtilities.getRecordSeries(rmService, nodeService, "Reports"); setComplete(); endTransaction(); @@ -1512,7 +1512,7 @@ public class DOD5015Test extends BaseSpringTest implements RecordsManagementMode */ public void testDispositionLifecycle_0318_reschedule_deletion_folderlevel() throws Exception { - final NodeRef recordSeries = TestUtilities.getRecordSeries(searchService, "Reports"); + final NodeRef recordSeries = TestUtilities.getRecordSeries(rmService, nodeService, "Reports"); setComplete(); endTransaction(); @@ -1836,7 +1836,7 @@ public class DOD5015Test extends BaseSpringTest implements RecordsManagementMode */ public void testDispositionLifecycle_0318_reschedule_deletion_recordlevel() throws Exception { - final NodeRef recordSeries = TestUtilities.getRecordSeries(searchService, "Reports"); + final NodeRef recordSeries = TestUtilities.getRecordSeries(rmService, nodeService, "Reports"); setComplete(); endTransaction(); @@ -2014,7 +2014,7 @@ public class DOD5015Test extends BaseSpringTest implements RecordsManagementMode */ public void testDispositionLifecycle_0318_existingfolders() throws Exception { - final NodeRef recordSeries = TestUtilities.getRecordSeries(searchService, "Reports"); + final NodeRef recordSeries = TestUtilities.getRecordSeries(rmService, nodeService, "Reports"); setComplete(); endTransaction(); @@ -2114,7 +2114,7 @@ public class DOD5015Test extends BaseSpringTest implements RecordsManagementMode */ public void testFolderLevelDispositionScheduleUpdate() throws Exception { - final NodeRef recordSeries = TestUtilities.getRecordSeries(searchService, "Reports"); + final NodeRef recordSeries = TestUtilities.getRecordSeries(rmService, nodeService, "Reports"); setComplete(); endTransaction(); @@ -2193,7 +2193,7 @@ public class DOD5015Test extends BaseSpringTest implements RecordsManagementMode */ public void testRecordLevelDispositionScheduleUpdate() throws Exception { - final NodeRef recordSeries = TestUtilities.getRecordSeries(searchService, "Reports"); + final NodeRef recordSeries = TestUtilities.getRecordSeries(rmService, nodeService, "Reports"); setComplete(); endTransaction(); @@ -2295,7 +2295,7 @@ public class DOD5015Test extends BaseSpringTest implements RecordsManagementMode public void testUnCutoff() { - final NodeRef recordCategory = TestUtilities.getRecordCategory(searchService, "Reports", "AIS Audit Records"); + final NodeRef recordCategory = TestUtilities.getRecordCategory(rmService, nodeService, "Reports", "AIS Audit Records"); setComplete(); endTransaction(); @@ -2400,7 +2400,7 @@ public class DOD5015Test extends BaseSpringTest implements RecordsManagementMode public void testFreeze() throws Exception { - final NodeRef recordCategory = TestUtilities.getRecordCategory(this.searchService, "Reports", "AIS Audit Records"); + final NodeRef recordCategory = TestUtilities.getRecordCategory(rmService, nodeService, "Reports", "AIS Audit Records"); assertNotNull(recordCategory); assertEquals("AIS Audit Records", this.nodeService.getProperty(recordCategory, ContentModel.PROP_NAME)); @@ -2711,7 +2711,7 @@ public class DOD5015Test extends BaseSpringTest implements RecordsManagementMode public void testAutoSuperseded() { - final NodeRef recordCategory = TestUtilities.getRecordCategory(this.searchService, "Civilian Files", "Employee Performance File System Records"); + final NodeRef recordCategory = TestUtilities.getRecordCategory(rmService, nodeService, "Civilian Files", "Employee Performance File System Records"); assertNotNull(recordCategory); assertEquals("Employee Performance File System Records", this.nodeService.getProperty(recordCategory, ContentModel.PROP_NAME)); @@ -2804,7 +2804,7 @@ public class DOD5015Test extends BaseSpringTest implements RecordsManagementMode public void testVersioned() { - final NodeRef recordCategory = TestUtilities.getRecordCategory(this.searchService, "Civilian Files", "Employee Performance File System Records"); + final NodeRef recordCategory = TestUtilities.getRecordCategory(rmService, nodeService, "Civilian Files", "Employee Performance File System Records"); assertNotNull(recordCategory); assertEquals("Employee Performance File System Records", this.nodeService.getProperty(recordCategory, ContentModel.PROP_NAME)); @@ -2878,7 +2878,7 @@ public class DOD5015Test extends BaseSpringTest implements RecordsManagementMode public void testDispositionLifecycle_0430_02_transfer() throws Exception { - final NodeRef recordCategory = TestUtilities.getRecordCategory(this.searchService, "Civilian Files", "Foreign Employee Award Files"); + final NodeRef recordCategory = TestUtilities.getRecordCategory(rmService, nodeService, "Civilian Files", "Foreign Employee Award Files"); assertNotNull(recordCategory); assertEquals("Foreign Employee Award Files", this.nodeService.getProperty(recordCategory, ContentModel.PROP_NAME)); @@ -3129,7 +3129,7 @@ public class DOD5015Test extends BaseSpringTest implements RecordsManagementMode public void testDispositionLifecycle_0430_01_recordleveldisposition() throws Exception { - NodeRef recordCategory = TestUtilities.getRecordCategory(this.searchService, "Civilian Files", "Employee Performance File System Records"); + NodeRef recordCategory = TestUtilities.getRecordCategory(rmService, nodeService, "Civilian Files", "Employee Performance File System Records"); assertNotNull(recordCategory); assertEquals("Employee Performance File System Records", this.nodeService.getProperty(recordCategory, ContentModel.PROP_NAME)); @@ -3225,7 +3225,7 @@ public class DOD5015Test extends BaseSpringTest implements RecordsManagementMode public void testDispositionLifecycle_0412_03_eventtest() throws Exception { - NodeRef recordCategory = TestUtilities.getRecordCategory(this.searchService, "Military Files", "Personnel Security Program Records"); + NodeRef recordCategory = TestUtilities.getRecordCategory(rmService, nodeService, "Military Files", "Personnel Security Program Records"); assertNotNull(recordCategory); assertEquals("Personnel Security Program Records", this.nodeService.getProperty(recordCategory, ContentModel.PROP_NAME)); @@ -3421,7 +3421,7 @@ public class DOD5015Test extends BaseSpringTest implements RecordsManagementMode */ public void testFileDOD5015CustomTypes() throws Exception { - NodeRef recordCategory = TestUtilities.getRecordCategory(this.searchService, "Reports", "AIS Audit Records"); + NodeRef recordCategory = TestUtilities.getRecordCategory(rmService, nodeService, "Reports", "AIS Audit Records"); NodeRef recordFolder = createRecordFolder(recordCategory, "March AIS Audit Records"); setComplete(); @@ -3458,7 +3458,7 @@ public class DOD5015Test extends BaseSpringTest implements RecordsManagementMode public void testFileDOD5015CustomTypes2() throws Exception { - NodeRef recordCategory = TestUtilities.getRecordCategory(this.searchService, "Reports", "AIS Audit Records"); + NodeRef recordCategory = TestUtilities.getRecordCategory(rmService, nodeService, "Reports", "AIS Audit Records"); NodeRef recordFolder = createRecordFolder(recordCategory, "March AIS Audit Records"); setComplete(); @@ -3494,7 +3494,7 @@ public class DOD5015Test extends BaseSpringTest implements RecordsManagementMode public void testFileFromDoclib() throws Exception { // Get the relevant RecordCategory and create a RecordFolder underneath it. - NodeRef recordCategory = TestUtilities.getRecordCategory(this.searchService, "Reports", "AIS Audit Records"); + NodeRef recordCategory = TestUtilities.getRecordCategory(rmService, nodeService, "Reports", "AIS Audit Records"); NodeRef recordFolder = createRecordFolder(recordCategory, "March AIS Audit Records"); setComplete(); @@ -3565,7 +3565,7 @@ public class DOD5015Test extends BaseSpringTest implements RecordsManagementMode public NodeRef execute() throws Throwable { // Get the relevant RecordCategory and create a RecordFolder underneath it. - NodeRef recordCategory = TestUtilities.getRecordCategory(searchService, "Reports", "AIS Audit Records"); + NodeRef recordCategory = TestUtilities.getRecordCategory(rmService, nodeService, "Reports", "AIS Audit Records"); NodeRef result = createRecordFolder(recordCategory, "March AIS Audit Records" + System.currentTimeMillis()); return result; @@ -3717,7 +3717,7 @@ public class DOD5015Test extends BaseSpringTest implements RecordsManagementMode // Create record category / record folder - final NodeRef recordCategory = TestUtilities.getRecordCategory(searchService, "Reports", "AIS Audit Records"); + final NodeRef recordCategory = TestUtilities.getRecordCategory(rmService, nodeService, "Reports", "AIS Audit Records"); assertNotNull(recordCategory); assertEquals("AIS Audit Records", nodeService.getProperty(recordCategory, ContentModel.PROP_NAME)); @@ -4272,7 +4272,7 @@ public class DOD5015Test extends BaseSpringTest implements RecordsManagementMode */ public void testETHREEOH3587() { - NodeRef recordFolder = TestUtilities.getRecordFolder(searchService, "Reports", "AIS Audit Records", "January AIS Audit Records"); + NodeRef recordFolder = TestUtilities.getRecordFolder(rmService, nodeService, "Reports", "AIS Audit Records", "January AIS Audit Records"); assertNotNull(recordFolder); // Create a record @@ -4323,7 +4323,7 @@ public class DOD5015Test extends BaseSpringTest implements RecordsManagementMode // TODO Don't think I need to do this. Can I reuse the existing January one? NodeRef vitalRecCategory = - TestUtilities.getRecordCategory(this.searchService, "Reports", "AIS Audit Records"); + TestUtilities.getRecordCategory(rmService, nodeService, "Reports", "AIS Audit Records"); assertNotNull(vitalRecCategory); assertEquals("AIS Audit Records", @@ -4389,7 +4389,7 @@ public class DOD5015Test extends BaseSpringTest implements RecordsManagementMode // // Create a record folder under a "non-vital" category // - NodeRef nonVitalRecordCategory = TestUtilities.getRecordCategory(this.searchService, "Reports", "Unit Manning Documents"); + NodeRef nonVitalRecordCategory = TestUtilities.getRecordCategory(rmService, nodeService, "Reports", "Unit Manning Documents"); assertNotNull(nonVitalRecordCategory); assertEquals("Unit Manning Documents", this.nodeService.getProperty(nonVitalRecordCategory, ContentModel.PROP_NAME)); diff --git a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/BaseCapabilitiesTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/BaseCapabilitiesTest.java deleted file mode 100644 index 7c429e4b58..0000000000 --- a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/BaseCapabilitiesTest.java +++ /dev/null @@ -1,922 +0,0 @@ -/* - * 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.module.org_alfresco_module_rm.test.capabilities; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import javax.transaction.UserTransaction; - -import junit.framework.TestCase; - -import org.alfresco.model.ContentModel; -import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; -import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService; -import org.alfresco.module.org_alfresco_module_rm.capability.Capability; -import org.alfresco.module.org_alfresco_module_rm.capability.CapabilityService; -import org.alfresco.module.org_alfresco_module_rm.capability.RMEntryVoter; -import org.alfresco.module.org_alfresco_module_rm.capability.RMPermissionModel; -import org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEventService; -import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; -import org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService; -import org.alfresco.repo.content.MimetypeMap; -import org.alfresco.repo.security.authentication.AuthenticationUtil; -import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; -import org.alfresco.repo.security.permissions.AccessDeniedException; -import org.alfresco.repo.security.permissions.PermissionReference; -import org.alfresco.repo.security.permissions.impl.model.PermissionModel; -import org.alfresco.repo.transaction.RetryingTransactionHelper; -import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; -import org.alfresco.service.cmr.repository.ChildAssociationRef; -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; -import org.alfresco.service.cmr.repository.StoreRef; -import org.alfresco.service.cmr.security.AccessStatus; -import org.alfresco.service.cmr.security.AuthorityService; -import org.alfresco.service.cmr.security.AuthorityType; -import org.alfresco.service.cmr.security.PermissionService; -import org.alfresco.service.cmr.security.PersonService; -import org.alfresco.service.namespace.NamespaceService; -import org.alfresco.service.namespace.QName; -import org.alfresco.service.namespace.RegexQNamePattern; -import org.alfresco.service.transaction.TransactionService; -import org.alfresco.util.ApplicationContextHelper; -import org.springframework.context.ApplicationContext; - -/** - * @author Roy Wetherall - */ -public abstract class BaseCapabilitiesTest extends TestCase - implements RMPermissionModel, RecordsManagementModel -{ - /* Application context */ - protected ApplicationContext ctx; - - /* Root node reference */ - protected StoreRef storeRef; - protected NodeRef rootNodeRef; - - /* Services */ - protected NodeService nodeService; - protected NodeService publicNodeService; - protected TransactionService transactionService; - protected PermissionService permissionService; - protected RecordsManagementService recordsManagementService; - protected RecordsManagementSecurityService recordsManagementSecurityService; - protected RecordsManagementActionService recordsManagementActionService; - protected RecordsManagementEventService recordsManagementEventService; - protected PermissionModel permissionModel; - protected ContentService contentService; - protected AuthorityService authorityService; - protected PersonService personService; - protected ContentService publicContentService; - protected RetryingTransactionHelper retryingTransactionHelper; - protected CapabilityService capabilityService; - - protected RMEntryVoter rmEntryVoter; - - protected UserTransaction testTX; - - protected NodeRef filePlan; - protected NodeRef recordSeries; - protected NodeRef recordCategory_1; - protected NodeRef recordCategory_2; - protected NodeRef recordFolder_1; - protected NodeRef recordFolder_2; - protected NodeRef record_1; - protected NodeRef record_2; - protected NodeRef recordCategory_3; - protected NodeRef recordFolder_3; - protected NodeRef record_3; - - protected String rmUsers; - protected String rmPowerUsers; - protected String rmSecurityOfficers; - protected String rmRecordsManagers; - protected String rmAdministrators; - - protected String rm_user; - protected String rm_power_user; - protected String rm_security_officer; - protected String rm_records_manager; - protected String rm_administrator; - protected String test_user; - - protected String testers; - - protected String[] stdUsers; - protected NodeRef[] stdNodeRefs;; - - /** - * Test setup - * @throws Exception - */ - protected void setUp() throws Exception - { - // Get the application context - ctx = ApplicationContextHelper.getApplicationContext(); - - // Get beans - nodeService = (NodeService) ctx.getBean("dbNodeService"); - publicNodeService = (NodeService) ctx.getBean("NodeService"); - transactionService = (TransactionService) ctx.getBean("transactionComponent"); - permissionService = (PermissionService) ctx.getBean("permissionService"); - permissionModel = (PermissionModel) ctx.getBean("permissionsModelDAO"); - contentService = (ContentService) ctx.getBean("contentService"); - publicContentService = (ContentService) ctx.getBean("ContentService"); - authorityService = (AuthorityService) ctx.getBean("authorityService"); - personService = (PersonService) ctx.getBean("personService"); - recordsManagementService = (RecordsManagementService) ctx.getBean("RecordsManagementService"); - recordsManagementSecurityService = (RecordsManagementSecurityService) ctx.getBean("RecordsManagementSecurityService"); - recordsManagementActionService = (RecordsManagementActionService) ctx.getBean("RecordsManagementActionService"); - recordsManagementEventService = (RecordsManagementEventService) ctx.getBean("RecordsManagementEventService"); - rmEntryVoter = (RMEntryVoter) ctx.getBean("rmEntryVoter"); - retryingTransactionHelper = (RetryingTransactionHelper)ctx.getBean("retryingTransactionHelper"); - capabilityService = (CapabilityService)ctx.getBean("capabilityService"); - - retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() - { - @Override - public Object execute() throws Throwable - { - // As system user - AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); - - // Create store and get the root node reference - storeRef = nodeService.createStore(StoreRef.PROTOCOL_WORKSPACE, "Test_" + System.currentTimeMillis()); - rootNodeRef = nodeService.getRootNode(storeRef); - - // As admin user - AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); - - // Create test events - recordsManagementEventService.getEvents(); - recordsManagementEventService.addEvent("rmEventType.simple", "event", "My Event"); - - // Create file plan node - filePlan = nodeService.createNode( - rootNodeRef, - ContentModel.ASSOC_CHILDREN, - TYPE_FILE_PLAN, - TYPE_FILE_PLAN).getChildRef(); - - return null; - } - }, false, true); - - - // Load in the plan data required for the test - loadFilePlanData(); - - retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() - { - @Override - public Object execute() throws Throwable - { - // As system user - AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); - - // create people ... - rm_user = "rm_user_" + storeRef.getIdentifier(); - rm_power_user = "rm_power_user_" + storeRef.getIdentifier(); - rm_security_officer = "rm_security_officer_" + storeRef.getIdentifier(); - rm_records_manager = "rm_records_manager_" + storeRef.getIdentifier(); - rm_administrator = "rm_administrator_" + storeRef.getIdentifier(); - test_user = "test_user_" + storeRef.getIdentifier(); - - personService.createPerson(createDefaultProperties(rm_user)); - personService.createPerson(createDefaultProperties(rm_power_user)); - personService.createPerson(createDefaultProperties(rm_security_officer)); - personService.createPerson(createDefaultProperties(rm_records_manager)); - personService.createPerson(createDefaultProperties(rm_administrator)); - personService.createPerson(createDefaultProperties(test_user)); - - // create roles as groups - rmUsers = authorityService.createAuthority(AuthorityType.GROUP, "RM_USER_" + storeRef.getIdentifier()); - rmPowerUsers = authorityService.createAuthority(AuthorityType.GROUP, "RM_POWER_USER_" + storeRef.getIdentifier()); - rmSecurityOfficers = authorityService.createAuthority(AuthorityType.GROUP, "RM_SECURITY_OFFICER_" + storeRef.getIdentifier()); - rmRecordsManagers = authorityService.createAuthority(AuthorityType.GROUP, "RM_RECORDS_MANAGER_" + storeRef.getIdentifier()); - rmAdministrators = authorityService.createAuthority(AuthorityType.GROUP, "RM_ADMINISTRATOR_" + storeRef.getIdentifier()); - testers = authorityService.createAuthority(AuthorityType.GROUP, "RM_TESTOR_" + storeRef.getIdentifier()); - - authorityService.addAuthority(testers, test_user); - - setPermissions(rmUsers, rm_user, ROLE_USER); - setPermissions(rmPowerUsers, rm_power_user, ROLE_POWER_USER); - setPermissions(rmSecurityOfficers, rm_security_officer, ROLE_SECURITY_OFFICER); - setPermissions(rmRecordsManagers, rm_records_manager, ROLE_RECORDS_MANAGER); - setPermissions(rmAdministrators, rm_administrator, ROLE_ADMINISTRATOR); - - stdUsers = new String[] - { - AuthenticationUtil.getSystemUserName(), - rm_administrator, - rm_records_manager, - rm_security_officer, - rm_power_user, - rm_user - }; - - stdNodeRefs = new NodeRef[] - { - recordFolder_1, - record_1, - recordFolder_2, - record_2 - }; - - return null; - } - }, false, true); - } - - /** - * Test tear down - * @throws Exception - */ - @Override - protected void tearDown() throws Exception - { - // TODO we should clean up as much as we can .... - } - - /** - * Set the permissions for a group, user and role - * @param group - * @param user - * @param role - */ - private void setPermissions(String group, String user, String role) - { - for (PermissionReference pr : permissionModel.getImmediateGranteePermissions(permissionModel.getPermissionReference(null, role))) - { - setPermission(filePlan, group, pr.getName(), true); - } - authorityService.addAuthority(group, user); - setPermission(filePlan, user, FILING, true); - } - - /** - * Loads the file plan date required for the tests - */ - protected void loadFilePlanData() - { - recordSeries = createRecordSeries(filePlan, "RS", "RS-1", "Record Series", "My record series"); - - recordCategory_1 = createRecordCategory(recordSeries, "Docs", "101-1", "Docs", "Docs", "week|1", true, false); - recordCategory_2 = createRecordCategory(recordSeries, "More Docs", "101-2", "More Docs", "More Docs", "week|1", true, true); - recordCategory_3 = createRecordCategory(recordSeries, "No disp schedule", "101-3", "No disp schedule", "No disp schedule", "week|1", true, null); - - recordFolder_1 = createRecordFolder(recordCategory_1, "F1", "101-3", "title", "description", "week|1", true); - recordFolder_2 = createRecordFolder(recordCategory_2, "F2", "102-3", "title", "description", "week|1", true); - recordFolder_3 = createRecordFolder(recordCategory_3, "F3", "103-3", "title", "description", "week|1", true); - - record_1 = createRecord(recordFolder_1); - record_2 = createRecord(recordFolder_2); - record_3 = createRecord(recordFolder_3); - } - - /** - * Set permission for authority on node reference. - * @param nodeRef - * @param authority - * @param permission - * @param allow - */ - private void setPermission(NodeRef nodeRef, String authority, String permission, boolean allow) - { - permissionService.setPermission(nodeRef, authority, permission, allow); - if (permission.equals(FILING)) - { - if (recordsManagementService.isRecordCategory(nodeRef) == true) - { - List assocs = nodeService.getChildAssocs(nodeRef, ContentModel.ASSOC_CONTAINS, RegexQNamePattern.MATCH_ALL); - for (ChildAssociationRef assoc : assocs) - { - NodeRef child = assoc.getChildRef(); - if (recordsManagementService.isRecordFolder(child) == true || - recordsManagementService.isRecordCategory(child) == true) - { - setPermission(child, authority, permission, allow); - } - } - } - } - } - - /** - * Create the default person properties - * @param userName - * @return - */ - private Map createDefaultProperties(String userName) - { - HashMap properties = new HashMap(); - properties.put(ContentModel.PROP_USERNAME, userName); - properties.put(ContentModel.PROP_HOMEFOLDER, null); - properties.put(ContentModel.PROP_FIRSTNAME, userName); - properties.put(ContentModel.PROP_LASTNAME, userName); - properties.put(ContentModel.PROP_EMAIL, userName); - properties.put(ContentModel.PROP_ORGID, ""); - return properties; - } - - /** - * Create a new record. Executed in a new transaction. - */ - private NodeRef createRecord(final NodeRef recordFolder) - { - return retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() - { - @Override - public NodeRef execute() throws Throwable - { - // As admin - AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); - - // Create the record - Map props = new HashMap(1); - props.put(ContentModel.PROP_NAME, "MyRecord.txt"); - NodeRef recordOne = nodeService.createNode(recordFolder, ContentModel.ASSOC_CONTAINS, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "MyRecord.txt"), - ContentModel.TYPE_CONTENT, props).getChildRef(); - - // Set the content - ContentWriter writer = contentService.getWriter(recordOne, ContentModel.PROP_CONTENT, true); - writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); - writer.setEncoding("UTF-8"); - writer.putContent("There is some content in this record"); - return recordOne; - } - }, false, true); - } - - /** - * Create a test record series. Executed in a new transaction. - */ - private NodeRef createRecordSeries(final NodeRef filePlan, final String name, final String identifier, final String title, final String description) - { - return retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() - { - @Override - public NodeRef execute() throws Throwable - { - // As admin - AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); - - HashMap properties = new HashMap(); - properties.put(ContentModel.PROP_NAME, name); - properties.put(PROP_IDENTIFIER, identifier); - properties.put(ContentModel.PROP_TITLE, title); - properties.put(ContentModel.PROP_DESCRIPTION, description); - - NodeRef recordSeried = nodeService.createNode(filePlan, ContentModel.ASSOC_CONTAINS, TYPE_RECORD_CATEGORY, TYPE_RECORD_CATEGORY, properties).getChildRef(); - permissionService.setInheritParentPermissions(recordSeried, false); - - return recordSeried; - } - }, false, true); - } - - /** - * Create a test record category in a new transaction. - */ - private NodeRef createRecordCategory( - final NodeRef recordSeries, - final String name, - final String identifier, - final String title, - final String description, - final String review, - final boolean vital, - final Boolean recordLevelDisposition) - { - return retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() - { - @Override - public NodeRef execute() throws Throwable - { - // As admin - AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); - - HashMap properties = new HashMap(); - properties.put(ContentModel.PROP_NAME, name); - properties.put(PROP_IDENTIFIER, identifier); - properties.put(ContentModel.PROP_TITLE, title); - properties.put(ContentModel.PROP_DESCRIPTION, description); - properties.put(PROP_REVIEW_PERIOD, review); - properties.put(PROP_VITAL_RECORD_INDICATOR, vital); - - NodeRef answer = nodeService.createNode(recordSeries, ContentModel.ASSOC_CONTAINS, TYPE_RECORD_CATEGORY, TYPE_RECORD_CATEGORY, properties) - .getChildRef(); - - if (recordLevelDisposition != null) - { - properties = new HashMap(); - properties.put(PROP_DISPOSITION_AUTHORITY, "N1-218-00-4 item 023"); - properties.put(PROP_DISPOSITION_INSTRUCTIONS, "Cut off monthly, hold 1 month, then destroy."); - properties.put(PROP_RECORD_LEVEL_DISPOSITION, recordLevelDisposition); - NodeRef ds = nodeService.createNode(answer, ASSOC_DISPOSITION_SCHEDULE, TYPE_DISPOSITION_SCHEDULE, TYPE_DISPOSITION_SCHEDULE, - properties).getChildRef(); - - createDispoistionAction(ds, "cutoff", "monthend|1", null, "event"); - createDispoistionAction(ds, "transfer", "month|1", null, null); - createDispoistionAction(ds, "accession", "month|1", null, null); - createDispoistionAction(ds, "destroy", "month|1", "{http://www.alfresco.org/model/recordsmanagement/1.0}cutOffDate", null); - } - - permissionService.setInheritParentPermissions(answer, false); - - return answer; - } - }, false, true); - } - - /** - * Create disposition action. - * @param disposition - * @param actionName - * @param period - * @param periodProperty - * @param event - * @return - */ - private NodeRef createDispoistionAction(NodeRef disposition, String actionName, String period, String periodProperty, String event) - { - HashMap properties = new HashMap(); - properties.put(PROP_DISPOSITION_ACTION_NAME, actionName); - properties.put(PROP_DISPOSITION_PERIOD, period); - if (periodProperty != null) - { - properties.put(PROP_DISPOSITION_PERIOD_PROPERTY, periodProperty); - } - if (event != null) - { - properties.put(PROP_DISPOSITION_EVENT, event); - } - NodeRef answer = nodeService.createNode(disposition, ASSOC_DISPOSITION_ACTION_DEFINITIONS, TYPE_DISPOSITION_ACTION_DEFINITION, - TYPE_DISPOSITION_ACTION_DEFINITION, properties).getChildRef(); - return answer; - } - - /** - * Create record folder. Executed in a new transaction. - * @param recordCategory - * @param name - * @param identifier - * @param title - * @param description - * @param review - * @param vital - * @return - */ - private NodeRef createRecordFolder( - final NodeRef recordCategory, - final String name, - final String identifier, - final String title, - final String description, - final String review, - final boolean vital) - { - return retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() - { - @Override - public NodeRef execute() throws Throwable - { - // As admin - AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); - - HashMap properties = new HashMap(); - properties.put(ContentModel.PROP_NAME, name); - properties.put(PROP_IDENTIFIER, identifier); - properties.put(ContentModel.PROP_TITLE, title); - properties.put(ContentModel.PROP_DESCRIPTION, description); - properties.put(PROP_REVIEW_PERIOD, review); - properties.put(PROP_VITAL_RECORD_INDICATOR, vital); - NodeRef answer = nodeService.createNode(recordCategory, ContentModel.ASSOC_CONTAINS, TYPE_RECORD_FOLDER, TYPE_RECORD_FOLDER, properties) - .getChildRef(); - permissionService.setInheritParentPermissions(answer, false); - return answer; - } - }, false, true); - } - - /** - * - * @param user - * @param nodeRef - * @param capabilityName - * @param accessStstus - */ - protected void checkCapability(final String user, final NodeRef nodeRef, final String capabilityName, final AccessStatus expected) - { - AuthenticationUtil.runAs(new RunAsWork() - { - @Override - public Object doWork() throws Exception - { - Capability capability = recordsManagementSecurityService.getCapability(capabilityName); - assertNotNull(capability); - - List capabilities = new ArrayList(1); - capabilities.add(capabilityName); - Map access = capabilityService.getCapabilitiesAccessState(nodeRef, capabilities); - - AccessStatus actual = access.get(capability); - - assertEquals( - "for user: " + user, - expected, - actual); - - return null; - } - }, user); - } - - /** - * - * @param access - * @param name - * @param accessStatus - */ - protected void check(Map access, String name, AccessStatus accessStatus) - { - Capability capability = recordsManagementSecurityService.getCapability(name); - assertNotNull(capability); - assertEquals(accessStatus, access.get(capability)); - } - - /** - * - * @param user - * @param nodeRef - * @param permission - * @param accessStstus - */ - protected void checkPermission(final String user, final NodeRef nodeRef, final String permission, final AccessStatus accessStstus) - { - AuthenticationUtil.runAs(new RunAsWork() - { - @Override - public Object doWork() throws Exception - { - AccessStatus actualAccessStatus = permissionService.hasPermission(nodeRef, permission); - assertTrue(actualAccessStatus == accessStstus); - return null; - } - }, user); - } - - /** - * - * @param nodeRef - * @param permission - * @param users - * @param expectedAccessStatus - */ - protected void checkPermissions( - final NodeRef nodeRef, - final String permission, - final String[] users, - final AccessStatus ... expectedAccessStatus) - { - retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() - { - @Override - public Object execute() throws Throwable - { - assertEquals( - "The number of users should match the number of expected access status", - users.length, - expectedAccessStatus.length); - - for (int i = 0; i < users.length; i++) - { - checkPermission(users[i], nodeRef, permission, expectedAccessStatus[i]); - } - - return null; - } - }, true, true); - } - - /** - * - * @param nodeRef - * @param capability - * @param users - * @param expectedAccessStatus - */ - protected void checkCapabilities( - final NodeRef nodeRef, - final String capability, - final String[] users, - final AccessStatus ... expectedAccessStatus) - { - retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() - { - @Override - public Object execute() throws Throwable - { - assertEquals( - "The number of users should match the number of expected access status", - users.length, - expectedAccessStatus.length); - - for (int i = 0; i < users.length; i++) - { - checkCapability(users[i], nodeRef, capability, expectedAccessStatus[i]); - } - - return null; - } - }, true, true); - } - - /** - * - * @param user - * @param capability - * @param nodeRefs - * @param expectedAccessStatus - */ - protected void checkCapabilities( - final String user, - final String capability, - final NodeRef[] nodeRefs, - final AccessStatus ... expectedAccessStatus) - { - retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() - { - @Override - public Object execute() throws Throwable - { - assertEquals( - "The number of node references should match the number of expected access status", - nodeRefs.length, - expectedAccessStatus.length); - - for (int i = 0; i < nodeRefs.length; i++) - { - checkCapability(user, nodeRefs[i], capability, expectedAccessStatus[i]); - } - - return null; - } - }, true, true); - } - - /** - * - * @param capability - * @param accessStatus - */ - protected void checkTestUserCapabilities(String capability, AccessStatus ... accessStatus) - { - checkCapabilities( - test_user, - capability, - stdNodeRefs, - accessStatus); - } - - /** - * Execute RM action - * @param action - * @param params - * @param nodeRefs - */ - protected void executeAction(final String action, final Map params, final String user, final NodeRef ... nodeRefs) - { - retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() - { - @Override - public Object execute() throws Throwable - { - AuthenticationUtil.setFullyAuthenticatedUser(user); - - for (NodeRef nodeRef : nodeRefs) - { - recordsManagementActionService.executeRecordsManagementAction(nodeRef, action, params); - } - - return null; - } - }, false, true); - } - - /** - * - * @param action - * @param nodeRefs - */ - protected void executeAction(final String action, final NodeRef ... nodeRefs) - { - executeAction(action, null, AuthenticationUtil.SYSTEM_USER_NAME, nodeRefs); - } - - /** - * - * @param action - * @param params - * @param nodeRefs - */ - protected void executeAction(final String action, final Map params, final NodeRef ... nodeRefs) - { - executeAction(action, params, AuthenticationUtil.SYSTEM_USER_NAME, nodeRefs); - } - - /** - * - * @param action - * @param params - * @param user - * @param nodeRefs - */ - protected void checkExecuteActionFail(final String action, final Map params, final String user, final NodeRef ... nodeRefs) - { - try - { - executeAction(action, params, user, nodeRefs); - fail("Action " + action + " has succeded and was expected to fail"); - } - catch (AccessDeniedException ade) - {} - } - - /** - * - * @param nodeRef - * @param property - * @param user - */ - protected void checkSetPropertyFail(final NodeRef nodeRef, final QName property, final String user, final Serializable value) - { - retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() - { - @Override - public Object execute() throws Throwable - { - AuthenticationUtil.setFullyAuthenticatedUser(user); - - try - { - publicNodeService.setProperty(nodeRef, property, value); - fail("Expected failure when setting property"); - } - catch (AccessDeniedException ade) - {} - - return null; - } - }, false, true); - } - - /** - * Add a capability - * @param capability - * @param authority - * @param nodeRefs - */ - protected void addCapability(final String capability, final String authority, final NodeRef ... nodeRefs) - { - retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() - { - @Override - public Object execute() throws Throwable - { - AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.SYSTEM_USER_NAME); - for (NodeRef nodeRef : nodeRefs) - { - permissionService.setPermission(nodeRef, authority, capability, true); - } - return null; - } - }, false, true); - } - - /** - * Remove capability - * @param capability - * @param authority - * @param nodeRef - */ - protected void removeCapability(final String capability, final String authority, final NodeRef ... nodeRefs) - { - retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() - { - @Override - public Object execute() throws Throwable - { - AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.SYSTEM_USER_NAME); - for (NodeRef nodeRef : nodeRefs) - { - permissionService.deletePermission(nodeRef, authority, capability); - } - return null; - } - }, false, true); - } - - /** - * - * @param nodeRefs - */ - protected void declare(final NodeRef ... nodeRefs) - { - retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() - { - @Override - public Object execute() throws Throwable - { - AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.SYSTEM_USER_NAME); - - for (NodeRef nodeRef : nodeRefs) - { - nodeService.setProperty(nodeRef, RecordsManagementModel.PROP_ORIGINATOR, "origValue"); - nodeService.setProperty(nodeRef, RecordsManagementModel.PROP_ORIGINATING_ORGANIZATION, "origOrgValue"); - nodeService.setProperty(nodeRef, RecordsManagementModel.PROP_PUBLICATION_DATE, new Date()); - nodeService.setProperty(nodeRef, ContentModel.PROP_TITLE, "titleValue"); - recordsManagementActionService.executeRecordsManagementAction(nodeRef, "declareRecord"); - } - - return null; - } - }, false, true); - } - - protected void cutoff(final NodeRef ... nodeRefs) - { - retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() - { - @Override - public Object execute() throws Throwable - { - AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.SYSTEM_USER_NAME); - - Calendar calendar = Calendar.getInstance(); - calendar.set(Calendar.HOUR, 0); - calendar.set(Calendar.MINUTE, 0); - calendar.set(Calendar.SECOND, 0); - - for (NodeRef nodeRef : nodeRefs) - { - NodeRef ndNodeRef = nodeService.getChildAssocs(nodeRef, RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); - nodeService.setProperty(ndNodeRef, RecordsManagementModel.PROP_DISPOSITION_AS_OF, calendar.getTime()); - recordsManagementActionService.executeRecordsManagementAction(nodeRef, "cutoff", null); - } - - return null; - } - }, false, true); - } - - protected void makeEligible(final NodeRef ... nodeRefs) - { - retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() - { - @Override - public Object execute() throws Throwable - { - AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.SYSTEM_USER_NAME); - - Calendar calendar = Calendar.getInstance(); - calendar.set(Calendar.HOUR, 0); - calendar.set(Calendar.MINUTE, 0); - calendar.set(Calendar.SECOND, 0); - - for (NodeRef nodeRef : nodeRefs) - { - NodeRef ndNodeRef = nodeService.getChildAssocs(nodeRef, RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); - nodeService.setProperty(ndNodeRef, RecordsManagementModel.PROP_DISPOSITION_AS_OF, calendar.getTime()); - } - - return null; - } - }, false, true); - } -} diff --git a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/CapabilitiesTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/CapabilitiesTest.java index ed97e266bd..3e7419f79e 100644 --- a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/CapabilitiesTest.java +++ b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/CapabilitiesTest.java @@ -71,7 +71,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements { super.setupTestDataImpl(); - record = createRecord(rmFolder, "CapabilitiesTest.txt"); + record = utils.createRecord(rmFolder, "CapabilitiesTest.txt"); } @Override @@ -1303,7 +1303,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements check(access, CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.ALLOWED); check(access, CREATE_MODIFY_DESTROY_FOLDERS, - AccessStatus.DENIED); + AccessStatus.ALLOWED); check(access, CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.ALLOWED); check(access, CREATE_MODIFY_DESTROY_REFERENCE_TYPES, @@ -1426,7 +1426,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements check(access, CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.ALLOWED); check(access, CREATE_MODIFY_DESTROY_FOLDERS, - AccessStatus.DENIED); + AccessStatus.ALLOWED); check(access, CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.ALLOWED); check(access, CREATE_MODIFY_DESTROY_REFERENCE_TYPES, @@ -1548,7 +1548,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements check(access, CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.ALLOWED); check(access, CREATE_MODIFY_DESTROY_FOLDERS, - AccessStatus.DENIED); + AccessStatus.ALLOWED); check(access, CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.ALLOWED); check(access, CREATE_MODIFY_DESTROY_REFERENCE_TYPES, @@ -1672,7 +1672,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements check(access, CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.ALLOWED); check(access, CREATE_MODIFY_DESTROY_FOLDERS, - AccessStatus.DENIED); + AccessStatus.ALLOWED); check(access, CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.ALLOWED); check(access, CREATE_MODIFY_DESTROY_REFERENCE_TYPES, @@ -1796,7 +1796,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements check(access, CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.DENIED); check(access, CREATE_MODIFY_DESTROY_FOLDERS, - AccessStatus.DENIED); + AccessStatus.ALLOWED); check(access, CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.DENIED); check(access, CREATE_MODIFY_DESTROY_REFERENCE_TYPES, @@ -1917,7 +1917,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements check(access, CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.DENIED); check(access, CREATE_MODIFY_DESTROY_FOLDERS, - AccessStatus.DENIED); + AccessStatus.ALLOWED); check(access, CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.DENIED); check(access, CREATE_MODIFY_DESTROY_REFERENCE_TYPES, @@ -2194,7 +2194,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements check(access, EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); check(access, EDIT_NON_RECORD_METADATA, - AccessStatus.ALLOWED); + AccessStatus.DENIED); check(access, EDIT_RECORD_METADATA, AccessStatus.DENIED); check(access, EDIT_SELECTION_LISTS, AccessStatus.ALLOWED); @@ -2318,7 +2318,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements check(access, EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); check(access, EDIT_NON_RECORD_METADATA, - AccessStatus.ALLOWED); + AccessStatus.DENIED); check(access, EDIT_RECORD_METADATA, AccessStatus.DENIED); check(access, EDIT_SELECTION_LISTS, AccessStatus.ALLOWED); @@ -2441,7 +2441,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements check(access, EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); check(access, EDIT_NON_RECORD_METADATA, - AccessStatus.ALLOWED); + AccessStatus.DENIED); check(access, EDIT_RECORD_METADATA, AccessStatus.DENIED); check(access, EDIT_SELECTION_LISTS, AccessStatus.ALLOWED); @@ -2562,7 +2562,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements check(access, EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); check(access, EDIT_NON_RECORD_METADATA, - AccessStatus.ALLOWED); + AccessStatus.DENIED); check(access, EDIT_RECORD_METADATA, AccessStatus.DENIED); check(access, EDIT_SELECTION_LISTS, AccessStatus.ALLOWED); @@ -2683,7 +2683,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements check(access, EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); check(access, EDIT_NON_RECORD_METADATA, - AccessStatus.ALLOWED); + AccessStatus.DENIED); check(access, EDIT_RECORD_METADATA, AccessStatus.DENIED); check(access, EDIT_SELECTION_LISTS, AccessStatus.DENIED); check(access, ENABLE_DISABLE_AUDIT_BY_TYPES, @@ -2801,7 +2801,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements check(access, EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); check(access, EDIT_NON_RECORD_METADATA, - AccessStatus.ALLOWED); + AccessStatus.DENIED); check(access, EDIT_RECORD_METADATA, AccessStatus.DENIED); check(access, EDIT_SELECTION_LISTS, AccessStatus.DENIED); check(access, ENABLE_DISABLE_AUDIT_BY_TYPES, @@ -3038,7 +3038,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements check(access, EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); check(access, EDIT_NON_RECORD_METADATA, - AccessStatus.DENIED); + AccessStatus.ALLOWED); check(access, EDIT_RECORD_METADATA, AccessStatus.ALLOWED); check(access, EDIT_SELECTION_LISTS, @@ -3162,7 +3162,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements check(access, EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); check(access, EDIT_NON_RECORD_METADATA, - AccessStatus.DENIED); + AccessStatus.ALLOWED); check(access, EDIT_RECORD_METADATA, AccessStatus.ALLOWED); check(access, EDIT_SELECTION_LISTS, @@ -3285,7 +3285,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements check(access, EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); check(access, EDIT_NON_RECORD_METADATA, - AccessStatus.DENIED); + AccessStatus.ALLOWED); check(access, EDIT_RECORD_METADATA, AccessStatus.ALLOWED); check(access, EDIT_SELECTION_LISTS, @@ -3409,7 +3409,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements check(access, EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); check(access, EDIT_NON_RECORD_METADATA, - AccessStatus.DENIED); + AccessStatus.ALLOWED); check(access, EDIT_RECORD_METADATA, AccessStatus.ALLOWED); check(access, EDIT_SELECTION_LISTS, @@ -3533,7 +3533,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements check(access, EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); check(access, EDIT_NON_RECORD_METADATA, - AccessStatus.DENIED); + AccessStatus.ALLOWED); check(access, EDIT_RECORD_METADATA, AccessStatus.ALLOWED); check(access, EDIT_SELECTION_LISTS, AccessStatus.DENIED); @@ -3655,7 +3655,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements check(access, EDIT_DECLARED_RECORD_METADATA, AccessStatus.DENIED); check(access, EDIT_NON_RECORD_METADATA, - AccessStatus.DENIED); + AccessStatus.ALLOWED); check(access, EDIT_RECORD_METADATA, AccessStatus.ALLOWED); check(access, EDIT_SELECTION_LISTS, AccessStatus.DENIED); diff --git a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/DeclarativeCapabilityTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/DeclarativeCapabilityTest.java index 27c65e35b4..2c63548ed6 100644 --- a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/DeclarativeCapabilityTest.java +++ b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/DeclarativeCapabilityTest.java @@ -63,16 +63,16 @@ public class DeclarativeCapabilityTest extends BaseRMTestCase super.setupTestDataImpl(); // Pre-filed content - record = createRecord(rmFolder, "record.txt"); - declaredRecord = createRecord(rmFolder, "declaredRecord.txt"); + record = utils.createRecord(rmFolder, "record.txt"); + declaredRecord = utils.createRecord(rmFolder, "declaredRecord.txt"); // Open folder // Closed folder recordFolderContainsFrozen = rmService.createRecordFolder(rmContainer, "containsFrozen"); - frozenRecord = createRecord(rmFolder, "frozenRecord.txt"); - frozenRecord2 = createRecord(recordFolderContainsFrozen, "frozen2.txt"); + frozenRecord = utils.createRecord(rmFolder, "frozenRecord.txt"); + frozenRecord2 = utils.createRecord(recordFolderContainsFrozen, "frozen2.txt"); frozenRecordFolder = rmService.createRecordFolder(rmContainer, "frozenRecordFolder"); } @@ -89,12 +89,12 @@ public class DeclarativeCapabilityTest extends BaseRMTestCase { AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); - declareRecord(declaredRecord); - declareRecord(frozenRecord); - declareRecord(frozenRecord2); - freeze(frozenRecord); - freeze(frozenRecordFolder); - freeze(frozenRecord2); + utils.declareRecord(declaredRecord); + utils.declareRecord(frozenRecord); + utils.declareRecord(frozenRecord2); + utils.freeze(frozenRecord); + utils.freeze(frozenRecordFolder); + utils.freeze(frozenRecord2); return null; } @@ -105,9 +105,9 @@ public class DeclarativeCapabilityTest extends BaseRMTestCase protected void tearDownImpl() { // Unfreeze stuff so it can be deleted - unfreeze(frozenRecord); - unfreeze(frozenRecordFolder); - unfreeze(frozenRecord2); + utils.unfreeze(frozenRecord); + utils.unfreeze(frozenRecordFolder); + utils.unfreeze(frozenRecord2); super.tearDownImpl(); } diff --git a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/jscript/JSONConversionComponentTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/jscript/JSONConversionComponentTest.java index 9569e77008..ab7d25b4ca 100644 --- a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/jscript/JSONConversionComponentTest.java +++ b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/jscript/JSONConversionComponentTest.java @@ -49,7 +49,7 @@ public class JSONConversionComponentTest extends BaseRMTestCase super.setupTestDataImpl(); // Create records - record = createRecord(rmFolder, "testRecord.txt"); + record = utils.createRecord(rmFolder, "testRecord.txt"); } public void testJSON() throws Exception diff --git a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/jscript/RMJScriptTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/jscript/RMJScriptTest.java index 1f9fe06d7a..a1326d4680 100644 --- a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/jscript/RMJScriptTest.java +++ b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/jscript/RMJScriptTest.java @@ -40,18 +40,29 @@ public class RMJScriptTest extends BaseRMTestCase @Override protected void initServices() { + super.initServices(); this.scriptService = (ScriptService)this.applicationContext.getBean("ScriptService"); } + private NodeRef record; public void testCapabilities() throws Exception { + doTestInTransaction(new Test() + { + @Override + public Void run() + { + record = utils.createRecord(rmFolder, "testRecord.txt"); + return null; + } + }); + doTestInTransaction(new Test() { @Override public NodeRef run() { - NodeRef record = createRecord(rmFolder, "testRecord.txt"); - declareRecord(record); + utils.declareRecord(record); return record; } diff --git a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/service/DispositionServiceImplTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/service/DispositionServiceImplTest.java index a81f7594e1..4eb6cf314d 100644 --- a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/service/DispositionServiceImplTest.java +++ b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/service/DispositionServiceImplTest.java @@ -37,6 +37,7 @@ import org.alfresco.module.org_alfresco_module_rm.job.publish.PublishExecutor; import org.alfresco.module.org_alfresco_module_rm.job.publish.PublishExecutorRegistry; import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; import org.alfresco.module.org_alfresco_module_rm.test.util.BaseRMTestCase; +import org.alfresco.module.org_alfresco_module_rm.test.util.CommonRMTestUtils; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.namespace.QName; @@ -148,7 +149,7 @@ public class DispositionServiceImplTest extends BaseRMTestCase { DispositionSchedule ds = dispositionService.getDispositionSchedule(container); assertNotNull(ds); - checkDispositionSchedule(ds, dispositionInstructions, DEFAULT_DISPOSITION_AUTHORITY, isRecordLevel); + checkDispositionSchedule(ds, dispositionInstructions, CommonRMTestUtils.DEFAULT_DISPOSITION_AUTHORITY, isRecordLevel); } private void doCheckFolder(NodeRef container, String dispositionInstructions, boolean isRecordLevel) @@ -193,7 +194,7 @@ public class DispositionServiceImplTest extends BaseRMTestCase */ private void checkDispositionSchedule(DispositionSchedule ds, boolean isRecordLevel) { - checkDispositionSchedule(ds, DEFAULT_DISPOSITION_INSTRUCTIONS, DEFAULT_DISPOSITION_AUTHORITY, isRecordLevel); + checkDispositionSchedule(ds, CommonRMTestUtils.DEFAULT_DISPOSITION_INSTRUCTIONS, CommonRMTestUtils.DEFAULT_DISPOSITION_AUTHORITY, isRecordLevel); } /** @@ -266,7 +267,7 @@ public class DispositionServiceImplTest extends BaseRMTestCase { DispositionSchedule ds = dispositionService.getAssociatedDispositionSchedule(container); assertNotNull(ds); - checkDispositionSchedule(ds, dispositionInstructions, DEFAULT_DISPOSITION_AUTHORITY, isRecordLevel); + checkDispositionSchedule(ds, dispositionInstructions, CommonRMTestUtils.DEFAULT_DISPOSITION_AUTHORITY, isRecordLevel); } }); } @@ -283,7 +284,7 @@ public class DispositionServiceImplTest extends BaseRMTestCase { // Add a new disposition schedule NodeRef container = rmService.createRecordCategory(rmContainer, "hasDisposableTest"); - DispositionSchedule ds = createBasicDispositionSchedule(container); + DispositionSchedule ds = utils.createBasicDispositionSchedule(container); assertTrue(dispositionService.hasDisposableItems(dispositionSchedule)); assertFalse(dispositionService.hasDisposableItems(ds)); @@ -387,7 +388,7 @@ public class DispositionServiceImplTest extends BaseRMTestCase NodeRef container = rmService.createRecordCategory(filePlan, "testCreateDispositionSchedule"); // Create a new disposition schedule - createBasicDispositionSchedule(container, "testCreateDispositionSchedule", "testCreateDispositionSchedule", false, true); + utils.createBasicDispositionSchedule(container, "testCreateDispositionSchedule", "testCreateDispositionSchedule", false, true); return container; } @@ -413,7 +414,7 @@ public class DispositionServiceImplTest extends BaseRMTestCase @Override public void run() { - createBasicDispositionSchedule(rmContainer); + utils.createBasicDispositionSchedule(rmContainer); } }); } @@ -434,8 +435,8 @@ public class DispositionServiceImplTest extends BaseRMTestCase NodeRef testB = rmService.createRecordCategory(testA, "testB"); // Create new disposition schedules - createBasicDispositionSchedule(testA, "testA", "testA", false, true); - createBasicDispositionSchedule(testB, "testB", "testB", false, true); + utils.createBasicDispositionSchedule(testA, "testA", "testA", false, true); + utils.createBasicDispositionSchedule(testB, "testB", "testB", false, true); // Add created containers to model setNodeRef("testA", testA); @@ -468,7 +469,7 @@ public class DispositionServiceImplTest extends BaseRMTestCase @Override public void run() { - createBasicDispositionSchedule(mhContainer11); + utils.createBasicDispositionSchedule(mhContainer11); } }); @@ -481,7 +482,7 @@ public class DispositionServiceImplTest extends BaseRMTestCase @Override public void run() { - createBasicDispositionSchedule(mhContainer21); + utils.createBasicDispositionSchedule(mhContainer21); } }); } @@ -554,8 +555,8 @@ public class DispositionServiceImplTest extends BaseRMTestCase @Override public Void run() throws Exception { - record43 = createRecord(mhRecordFolder43, "record1.txt"); - record45 = createRecord(mhRecordFolder45, "record2.txt"); + record43 = utils.createRecord(mhRecordFolder43, "record1.txt"); + record45 = utils.createRecord(mhRecordFolder45, "record2.txt"); return null; } @@ -692,8 +693,8 @@ public class DispositionServiceImplTest extends BaseRMTestCase checkDispositionAction( dispositionService.getNextDispositionAction(recordFolder), "cutoff", - new String[]{DEFAULT_EVENT_NAME}, - PERIOD_NONE); + new String[]{CommonRMTestUtils.DEFAULT_EVENT_NAME}, + CommonRMTestUtils.PERIOD_NONE); } private void checkDisposableItemChanged(NodeRef recordFolder) throws Exception @@ -701,7 +702,7 @@ public class DispositionServiceImplTest extends BaseRMTestCase checkDispositionAction( dispositionService.getNextDispositionAction(recordFolder), "cutoff", - new String[]{DEFAULT_EVENT_NAME, "abolished"}, + new String[]{CommonRMTestUtils.DEFAULT_EVENT_NAME, "abolished"}, "week|1"); } @@ -709,7 +710,7 @@ public class DispositionServiceImplTest extends BaseRMTestCase { Map updateProps = new HashMap(3); updateProps.put(PROP_DISPOSITION_PERIOD, "week|1"); - updateProps.put(PROP_DISPOSITION_EVENT, (Serializable)Arrays.asList(DEFAULT_EVENT_NAME, "abolished")); + updateProps.put(PROP_DISPOSITION_EVENT, (Serializable)Arrays.asList(CommonRMTestUtils.DEFAULT_EVENT_NAME, "abolished")); DispositionSchedule ds = dispositionService.getDispositionSchedule(nodeRef); DispositionActionDefinition dad = ds.getDispositionActionDefinitionByName("cutoff"); @@ -777,7 +778,7 @@ public class DispositionServiceImplTest extends BaseRMTestCase fail(buff.toString()); } - if (PERIOD_NONE.equals(strPeriod) == true) + if (CommonRMTestUtils.PERIOD_NONE.equals(strPeriod) == true) { assertNull(da.getAsOfDate()); } diff --git a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementAdminServiceImplTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementAdminServiceImplTest.java index 9574d9d72f..7b07fc1c2a 100644 --- a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementAdminServiceImplTest.java +++ b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementAdminServiceImplTest.java @@ -562,7 +562,7 @@ public class RecordsManagementAdminServiceImplTest extends BaseRMTestCase { public NodeRef execute() throws Throwable { - NodeRef result = createRecord(rmFolder, "testRecordA" + System.currentTimeMillis()); + NodeRef result = utils.createRecord(rmFolder, "testRecordA" + System.currentTimeMillis()); return result; } }); @@ -570,7 +570,7 @@ public class RecordsManagementAdminServiceImplTest extends BaseRMTestCase { public NodeRef execute() throws Throwable { - NodeRef result = createRecord(rmFolder, "testRecordB" + System.currentTimeMillis()); + NodeRef result = utils.createRecord(rmFolder, "testRecordB" + System.currentTimeMillis()); return result; } }); @@ -579,8 +579,8 @@ public class RecordsManagementAdminServiceImplTest extends BaseRMTestCase { public QName execute() throws Throwable { - declareRecord(testRecord1); - declareRecord(testRecord2); + utils.declareRecord(testRecord1); + utils.declareRecord(testRecord2); Map params = new HashMap(); params.put("referenceType", refType.toString()); @@ -758,8 +758,8 @@ public class RecordsManagementAdminServiceImplTest extends BaseRMTestCase { public Pair execute() throws Throwable { - NodeRef rec1 = createRecord(rmFolder, "testRecordA" + System.currentTimeMillis()); - NodeRef rec2 = createRecord(rmFolder, "testRecordB" + System.currentTimeMillis()); + NodeRef rec1 = utils.createRecord(rmFolder, "testRecordA" + System.currentTimeMillis()); + NodeRef rec2 = utils.createRecord(rmFolder, "testRecordB" + System.currentTimeMillis()); Pair result = new Pair(rec1, rec2); return result; } @@ -771,8 +771,8 @@ public class RecordsManagementAdminServiceImplTest extends BaseRMTestCase { public Void execute() throws Throwable { - declareRecord(testRecord1); - declareRecord(testRecord2); + utils.declareRecord(testRecord1); + utils.declareRecord(testRecord2); policyComponent.bindClassBehaviour( RecordsManagementPolicies.BEFORE_CREATE_REFERENCE, diff --git a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementSearchServiceImplTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementSearchServiceImplTest.java index 8ad3c91911..085ef35879 100644 --- a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementSearchServiceImplTest.java +++ b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementSearchServiceImplTest.java @@ -107,12 +107,12 @@ public class RecordsManagementSearchServiceImplTest extends BaseRMTestCase folderLevelRecordFolder = mhRecordFolder42; recordLevelRecordFolder = mhRecordFolder43; - recordOne = createRecord(folderLevelRecordFolder, "recordOne.txt", null, "record one - folder level - elephant"); - recordTwo = createRecord(folderLevelRecordFolder, "recordTwo.txt", null, "record two - folder level - snake"); - recordThree = createRecord(folderLevelRecordFolder, "recordThree.txt", null, "record three - folder level - monkey"); - recordFour = createRecord(recordLevelRecordFolder, "recordFour.txt", null, "record four - record level - elephant"); - recordFive = createRecord(recordLevelRecordFolder, "recordFive.txt", null, "record five - record level - snake"); - recordSix = createRecord(recordLevelRecordFolder, "recordSix.txt", null, "record six - record level - monkey"); + recordOne = utils.createRecord(folderLevelRecordFolder, "recordOne.txt", null, "record one - folder level - elephant"); + recordTwo = utils.createRecord(folderLevelRecordFolder, "recordTwo.txt", null, "record two - folder level - snake"); + recordThree = utils.createRecord(folderLevelRecordFolder, "recordThree.txt", null, "record three - folder level - monkey"); + recordFour = utils.createRecord(recordLevelRecordFolder, "recordFour.txt", null, "record four - record level - elephant"); + recordFive = utils.createRecord(recordLevelRecordFolder, "recordFive.txt", null, "record five - record level - snake"); + recordSix = utils.createRecord(recordLevelRecordFolder, "recordSix.txt", null, "record six - record level - monkey"); return null; } diff --git a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementServiceImplTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementServiceImplTest.java index 3c7f77db0d..f9d818bdaa 100644 --- a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementServiceImplTest.java +++ b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/service/RecordsManagementServiceImplTest.java @@ -69,7 +69,7 @@ public class RecordsManagementServiceImplTest extends BaseRMTestCase @Override public NodeRef run() throws Exception { - return createRecord(rmFolder, "testRecord.txt"); + return utils.createRecord(rmFolder, "testRecord.txt"); } @Override diff --git a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/service/VitalRecordServiceImplTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/service/VitalRecordServiceImplTest.java index ed7ff8cdc8..ce07978310 100644 --- a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/service/VitalRecordServiceImplTest.java +++ b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/service/VitalRecordServiceImplTest.java @@ -100,11 +100,11 @@ public class VitalRecordServiceImplTest extends BaseRMTestCase @Override public Object execute() throws Throwable { - mhRecord51 = createRecord(mhRecordFolder41, "record51.txt"); - mhRecord52 = createRecord(mhRecordFolder42, "record52.txt"); - mhRecord53 = createRecord(mhRecordFolder43, "record53.txt"); - mhRecord54 = createRecord(mhRecordFolder44, "record54.txt"); - mhRecord55 = createRecord(mhRecordFolder45, "record55.txt"); + mhRecord51 = utils.createRecord(mhRecordFolder41, "record51.txt"); + mhRecord52 = utils.createRecord(mhRecordFolder42, "record52.txt"); + mhRecord53 = utils.createRecord(mhRecordFolder43, "record53.txt"); + mhRecord54 = utils.createRecord(mhRecordFolder44, "record54.txt"); + mhRecord55 = utils.createRecord(mhRecordFolder45, "record55.txt"); return null; } diff --git a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/system/DODDataLoadSystemTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/system/DODDataLoadSystemTest.java deleted file mode 100644 index ce1c02fc94..0000000000 --- a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/system/DODDataLoadSystemTest.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * 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.module.org_alfresco_module_rm.test.system; - -import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; -import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService; -import org.alfresco.module.org_alfresco_module_rm.test.util.TestUtilities; -import org.alfresco.repo.security.authentication.AuthenticationComponent; -import org.alfresco.repo.security.authentication.AuthenticationUtil; -import org.alfresco.service.cmr.repository.NodeService; -import org.alfresco.service.cmr.search.SearchService; -import org.alfresco.service.cmr.security.PermissionService; -import org.alfresco.service.cmr.view.ImporterService; -import org.alfresco.util.BaseSpringTest; - -/** - * - * - * @author Roy Wetherall - */ -public class DODDataLoadSystemTest extends BaseSpringTest -{ - private NodeService nodeService; - private AuthenticationComponent authenticationComponent; - private ImporterService importer; - private PermissionService permissionService; - private SearchService searchService; - private RecordsManagementService rmService; - private RecordsManagementActionService rmActionService; - - @Override - protected void onSetUpInTransaction() throws Exception - { - super.onSetUpInTransaction(); - - // Get the service required in the tests - this.nodeService = (NodeService)this.applicationContext.getBean("NodeService"); - this.authenticationComponent = (AuthenticationComponent)this.applicationContext.getBean("authenticationComponent"); - this.importer = (ImporterService)this.applicationContext.getBean("ImporterService"); - this.permissionService = (PermissionService)this.applicationContext.getBean("PermissionService"); - searchService = (SearchService)applicationContext.getBean("SearchService"); - rmService = (RecordsManagementService)applicationContext.getBean("RecordsManagementService"); - rmActionService = (RecordsManagementActionService)applicationContext.getBean("RecordsManagementActionService"); - - - // Set the current security context as admin - this.authenticationComponent.setCurrentUser(AuthenticationUtil.getSystemUserName()); - } - - public void testSetup() - { - // NOOP - } - - public void testLoadFilePlanData() - { - TestUtilities.loadFilePlanData(applicationContext); - - setComplete(); - endTransaction(); - } -} diff --git a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/system/NotificationServiceHelperSystemTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/system/NotificationServiceHelperSystemTest.java index 6577dc21e1..dbdcc71dc3 100644 --- a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/system/NotificationServiceHelperSystemTest.java +++ b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/system/NotificationServiceHelperSystemTest.java @@ -111,9 +111,9 @@ public class NotificationServiceHelperSystemTest extends BaseRMTestCase super.setupTestDataImpl(); // Create a few test records - record = createRecord(rmFolder, "recordOne"); - NodeRef record2 = createRecord(rmFolder, "recordTwo"); - NodeRef record3 = createRecord(rmFolder, "recordThree"); + record = utils.createRecord(rmFolder, "recordOne"); + NodeRef record2 = utils.createRecord(rmFolder, "recordTwo"); + NodeRef record3 = utils.createRecord(rmFolder, "recordThree"); records = new ArrayList(3); records.add(record); diff --git a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/system/RecordsManagementServiceImplSystemTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/system/RecordsManagementServiceImplSystemTest.java index dd5f60c4d5..7ca0a52d65 100644 --- a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/system/RecordsManagementServiceImplSystemTest.java +++ b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/system/RecordsManagementServiceImplSystemTest.java @@ -175,7 +175,7 @@ public class RecordsManagementServiceImplSystemTest extends BaseSpringTest imple */ public void testRescheduleRecord_IsNotCutOff() throws Exception { - final NodeRef recCat = TestUtilities.getRecordCategory(searchService, "Reports", "AIS Audit Records"); + final NodeRef recCat = TestUtilities.getRecordCategory(rmService, nodeService, "Reports", "AIS Audit Records"); // This RC has disposition instructions "Cut off monthly, hold 1 month, then destroy." setComplete(); @@ -379,7 +379,7 @@ public class RecordsManagementServiceImplSystemTest extends BaseSpringTest imple { public Void execute() throws Throwable { - NodeRef folderRecord = TestUtilities.getRecordFolder(searchService, "Reports", "AIS Audit Records", "January AIS Audit Records"); + NodeRef folderRecord = TestUtilities.getRecordFolder(rmService, nodeService, "Reports", "AIS Audit Records", "January AIS Audit Records"); assertNotNull(folderRecord); assertEquals("January AIS Audit Records", nodeService.getProperty(folderRecord, ContentModel.PROP_NAME)); @@ -394,7 +394,7 @@ public class RecordsManagementServiceImplSystemTest extends BaseSpringTest imple assertFalse(di.isRecordLevelDisposition()); // Get a record category - NodeRef recordCategory = TestUtilities.getRecordCategory(searchService, "Reports", "AIS Audit Records"); + NodeRef recordCategory = TestUtilities.getRecordCategory(rmService, nodeService, "Reports", "AIS Audit Records"); assertNotNull(recordCategory); assertEquals("AIS Audit Records", nodeService.getProperty(recordCategory, ContentModel.PROP_NAME)); @@ -434,7 +434,7 @@ public class RecordsManagementServiceImplSystemTest extends BaseSpringTest imple { public NodeRef execute() throws Throwable { - NodeRef result = TestUtilities.getRecordFolder(searchService, "Civilian Files", "Case Files and Papers", "Gilbert Competency Hearing"); + NodeRef result = TestUtilities.getRecordFolder(rmService, nodeService, "Civilian Files", "Case Files and Papers", "Gilbert Competency Hearing"); assertNotNull("cleanRecordFolder was null", result); final DispositionSchedule dispositionSchedule = dispositionService.getDispositionSchedule(result); @@ -451,7 +451,7 @@ public class RecordsManagementServiceImplSystemTest extends BaseSpringTest imple { public NodeRef execute() throws Throwable { - NodeRef result = TestUtilities.getRecordFolder(searchService, "Reports", "AIS Audit Records", "January AIS Audit Records"); + NodeRef result = TestUtilities.getRecordFolder(rmService, nodeService, "Reports", "AIS Audit Records", "January AIS Audit Records"); assertNotNull("dispositionAndVitalRecordFolder was null", result); final DispositionSchedule dispositionSchedule = dispositionService.getDispositionSchedule(result); @@ -579,7 +579,7 @@ public class RecordsManagementServiceImplSystemTest extends BaseSpringTest imple { public NodeRef execute() throws Throwable { - NodeRef result = TestUtilities.getRecordFolder(searchService, "Civilian Files", "Case Files and Papers", "Gilbert Competency Hearing"); + NodeRef result = TestUtilities.getRecordFolder(rmService, nodeService, "Civilian Files", "Case Files and Papers", "Gilbert Competency Hearing"); assertNotNull("cleanRecordFolder was null", result); final DispositionSchedule dispositionSchedule = dispositionService.getDispositionSchedule(result); @@ -596,7 +596,7 @@ public class RecordsManagementServiceImplSystemTest extends BaseSpringTest imple { public NodeRef execute() throws Throwable { - NodeRef result = TestUtilities.getRecordFolder(searchService, "Reports", "AIS Audit Records", "January AIS Audit Records"); + NodeRef result = TestUtilities.getRecordFolder(rmService, nodeService, "Reports", "AIS Audit Records", "January AIS Audit Records"); assertNotNull("dispositionAndVitalRecordFolder was null", result); final DispositionSchedule dispositionSchedule = dispositionService.getDispositionSchedule(result); @@ -757,7 +757,7 @@ public class RecordsManagementServiceImplSystemTest extends BaseSpringTest imple public Void execute() throws Throwable { // Get a record folder - NodeRef folderRecord = TestUtilities.getRecordFolder(searchService, "Reports", "AIS Audit Records", "January AIS Audit Records"); + NodeRef folderRecord = TestUtilities.getRecordFolder(rmService, nodeService, "Reports", "AIS Audit Records", "January AIS Audit Records"); assertNotNull(folderRecord); assertEquals("January AIS Audit Records", nodeService.getProperty(folderRecord, ContentModel.PROP_NAME)); diff --git a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/util/BaseRMTestCase.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/util/BaseRMTestCase.java index 131302ffd8..28a10feff4 100644 --- a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/util/BaseRMTestCase.java +++ b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/util/BaseRMTestCase.java @@ -91,6 +91,9 @@ public abstract class BaseRMTestCase extends RetryingTransactionHelperTestCase /** Site id */ protected static final String SITE_ID = "mySite"; + /** Common test utils */ + protected CommonRMTestUtils utils; + /** Services */ protected NodeService nodeService; protected ContentService contentService; @@ -195,13 +198,6 @@ public abstract class BaseRMTestCase extends RetryingTransactionHelperTestCase protected NodeRef recordsManagerPerson; protected NodeRef rmAdminPerson; - /** test values */ - protected static final String DEFAULT_DISPOSITION_AUTHORITY = "disposition authority"; - protected static final String DEFAULT_DISPOSITION_INSTRUCTIONS = "disposition instructions"; - protected static final String DEFAULT_DISPOSITION_DESCRIPTION = "disposition action description"; - protected static final String DEFAULT_EVENT_NAME = "case_closed"; - protected static final String PERIOD_NONE = "none|0"; - /** * Indicates whether this is a multi-hierarchy test or not. If it is then the multi-hierarchy record * taxonomy test data is loaded. @@ -228,6 +224,7 @@ public abstract class BaseRMTestCase extends RetryingTransactionHelperTestCase { // Get the application context applicationContext = ApplicationContextHelper.getApplicationContext(CONFIG_LOCATIONS); + utils = new CommonRMTestUtils(applicationContext); // Initialise the service beans initServices(); @@ -365,7 +362,7 @@ public abstract class BaseRMTestCase extends RetryingTransactionHelperTestCase assertNotNull("Could not create rm container", rmContainer); // Create disposition schedule - dispositionSchedule = createBasicDispositionSchedule(rmContainer); + dispositionSchedule = utils.createBasicDispositionSchedule(rmContainer); // Create RM folder rmFolder = rmService.createRecordFolder(rmContainer, "rmFolder"); @@ -465,24 +462,24 @@ public abstract class BaseRMTestCase extends RetryingTransactionHelperTestCase // Level 1 mhContainer11 = rmService.createRecordCategory(mhContainer, "mhContainer11"); - mhDispositionSchedule11 = createBasicDispositionSchedule(mhContainer11, "ds11", DEFAULT_DISPOSITION_AUTHORITY, false, true); + mhDispositionSchedule11 = utils.createBasicDispositionSchedule(mhContainer11, "ds11", utils.DEFAULT_DISPOSITION_AUTHORITY, false, true); mhContainer12 = rmService.createRecordCategory(mhContainer, "mhContainer12"); - mhDispositionSchedule12 = createBasicDispositionSchedule(mhContainer12, "ds12", DEFAULT_DISPOSITION_AUTHORITY, false, true); + mhDispositionSchedule12 = utils.createBasicDispositionSchedule(mhContainer12, "ds12", utils.DEFAULT_DISPOSITION_AUTHORITY, false, true); // Level 2 mhContainer21 = rmService.createRecordCategory(mhContainer11, "mhContainer21"); mhContainer22 = rmService.createRecordCategory(mhContainer12, "mhContainer22"); mhContainer23 = rmService.createRecordCategory(mhContainer12, "mhContainer23"); - mhDispositionSchedule23 = createBasicDispositionSchedule(mhContainer23, "ds23", DEFAULT_DISPOSITION_AUTHORITY, false, true); + mhDispositionSchedule23 = utils.createBasicDispositionSchedule(mhContainer23, "ds23", utils.DEFAULT_DISPOSITION_AUTHORITY, false, true); // Level 3 mhContainer31 = rmService.createRecordCategory(mhContainer21, "mhContainer31"); mhContainer32 = rmService.createRecordCategory(mhContainer22, "mhContainer32"); mhContainer33 = rmService.createRecordCategory(mhContainer22, "mhContainer33"); - mhDispositionSchedule33 = createBasicDispositionSchedule(mhContainer33, "ds33", DEFAULT_DISPOSITION_AUTHORITY, true, true); + mhDispositionSchedule33 = utils.createBasicDispositionSchedule(mhContainer33, "ds33", utils.DEFAULT_DISPOSITION_AUTHORITY, true, true); mhContainer34 = rmService.createRecordCategory(mhContainer23, "mhContainer34"); mhContainer35 = rmService.createRecordCategory(mhContainer23, "mhContainer35"); - mhDispositionSchedule35 = createBasicDispositionSchedule(mhContainer35, "ds35", DEFAULT_DISPOSITION_AUTHORITY, true, true); + mhDispositionSchedule35 = utils.createBasicDispositionSchedule(mhContainer35, "ds35", utils.DEFAULT_DISPOSITION_AUTHORITY, true, true); // Record folders mhRecordFolder41 = rmService.createRecordFolder(mhContainer31, "mhFolder41"); @@ -491,145 +488,4 @@ public abstract class BaseRMTestCase extends RetryingTransactionHelperTestCase mhRecordFolder44 = rmService.createRecordFolder(mhContainer34, "mhFolder44"); mhRecordFolder45 = rmService.createRecordFolder(mhContainer35, "mhFolder45"); } - - /** - * - * @param container - * @return - */ - protected DispositionSchedule createBasicDispositionSchedule(NodeRef container) - { - return createBasicDispositionSchedule(container, DEFAULT_DISPOSITION_INSTRUCTIONS, DEFAULT_DISPOSITION_AUTHORITY, false, true); - } - - /** - * - * @param container - * @param isRecordLevel - * @param defaultDispositionActions - * @return - */ - protected DispositionSchedule createBasicDispositionSchedule( - NodeRef container, - String dispositionInstructions, - String dispositionAuthority, - boolean isRecordLevel, - boolean defaultDispositionActions) - { - Map dsProps = new HashMap(3); - dsProps.put(PROP_DISPOSITION_AUTHORITY, dispositionAuthority); - dsProps.put(PROP_DISPOSITION_INSTRUCTIONS, dispositionInstructions); - dsProps.put(PROP_RECORD_LEVEL_DISPOSITION, isRecordLevel); - DispositionSchedule dispositionSchedule = dispositionService.createDispositionSchedule(container, dsProps); - assertNotNull(dispositionSchedule); - - if (defaultDispositionActions == true) - { - Map adParams = new HashMap(3); - adParams.put(PROP_DISPOSITION_ACTION_NAME, "cutoff"); - adParams.put(PROP_DISPOSITION_DESCRIPTION, DEFAULT_DISPOSITION_DESCRIPTION); - - List events = new ArrayList(1); - events.add(DEFAULT_EVENT_NAME); - adParams.put(PROP_DISPOSITION_EVENT, (Serializable)events); - - dispositionService.addDispositionActionDefinition(dispositionSchedule, adParams); - - adParams = new HashMap(3); - adParams.put(PROP_DISPOSITION_ACTION_NAME, "destroy"); - adParams.put(PROP_DISPOSITION_DESCRIPTION, DEFAULT_DISPOSITION_DESCRIPTION); - adParams.put(PROP_DISPOSITION_PERIOD, "immediately|0"); - - dispositionService.addDispositionActionDefinition(dispositionSchedule, adParams); - } - - return dispositionSchedule; - } - - protected NodeRef createRecord(NodeRef recordFolder, String name) - { - return createRecord(recordFolder, name, null, "Some test content"); - } - - protected NodeRef createRecord(NodeRef recordFolder, String name, Map properties, String content) - { - // Create the document - if (properties == null) - { - properties = new HashMap(1); - } - if (properties.containsKey(ContentModel.PROP_NAME) == false) - { - properties.put(ContentModel.PROP_NAME, name); - } - NodeRef recordOne = this.nodeService.createNode(recordFolder, - ContentModel.ASSOC_CONTAINS, - QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, name), - ContentModel.TYPE_CONTENT, - properties).getChildRef(); - - // Set the content - ContentWriter writer = contentService.getWriter(recordOne, ContentModel.PROP_CONTENT, true); - writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); - writer.setEncoding("UTF-8"); - writer.putContent(content); - - return recordOne; - } - - protected void declareRecord(final NodeRef record) - { - AuthenticationUtil.runAs(new RunAsWork() - { - @Override - public Void doWork() throws Exception - { - // Declare record - nodeService.setProperty(record, RecordsManagementModel.PROP_PUBLICATION_DATE, new Date()); - nodeService.setProperty(record, RecordsManagementModel.PROP_MEDIA_TYPE, "mediaTypeValue"); - nodeService.setProperty(record, RecordsManagementModel.PROP_FORMAT, "formatValue"); - nodeService.setProperty(record, RecordsManagementModel.PROP_DATE_RECEIVED, new Date()); - nodeService.setProperty(record, RecordsManagementModel.PROP_DATE_FILED, new Date()); - nodeService.setProperty(record, RecordsManagementModel.PROP_ORIGINATOR, "origValue"); - nodeService.setProperty(record, RecordsManagementModel.PROP_ORIGINATING_ORGANIZATION, "origOrgValue"); - nodeService.setProperty(record, ContentModel.PROP_TITLE, "titleValue"); - actionService.executeRecordsManagementAction(record, "declareRecord"); - - return null; - } - - }, AuthenticationUtil.getAdminUserName()); - - } - - protected void freeze(final NodeRef nodeRef) - { - AuthenticationUtil.runAs(new RunAsWork() - { - @Override - public Void doWork() throws Exception - { - Map params = new HashMap(1); - params.put(FreezeAction.PARAM_REASON, "Freeze reason."); - actionService.executeRecordsManagementAction(nodeRef, "freeze", params); - - return null; - } - - }, AuthenticationUtil.getSystemUserName()); - } - - protected void unfreeze(final NodeRef nodeRef) - { - AuthenticationUtil.runAs(new RunAsWork() - { - @Override - public Void doWork() throws Exception - { - actionService.executeRecordsManagementAction(nodeRef, "unfreeze"); - return null; - } - - }, AuthenticationUtil.getSystemUserName()); - } } diff --git a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/util/BaseRMWebScriptTestCase.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/util/BaseRMWebScriptTestCase.java new file mode 100644 index 0000000000..f45947b6f1 --- /dev/null +++ b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/util/BaseRMWebScriptTestCase.java @@ -0,0 +1,236 @@ +/** + * + */ +package org.alfresco.module.org_alfresco_module_rm.test.util; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminService; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService; +import org.alfresco.module.org_alfresco_module_rm.audit.RecordsManagementAuditService; +import org.alfresco.module.org_alfresco_module_rm.capability.CapabilityService; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService; +import org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEventService; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.module.org_alfresco_module_rm.model.RmSiteType; +import org.alfresco.module.org_alfresco_module_rm.search.RecordsManagementSearchService; +import org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService; +import org.alfresco.module.org_alfresco_module_rm.vital.VitalRecordService; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.repo.web.scripts.BaseWebScriptTest; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.Period; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.search.SearchService; +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.cmr.site.SiteInfo; +import org.alfresco.service.cmr.site.SiteService; +import org.alfresco.service.cmr.site.SiteVisibility; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.springframework.context.ApplicationContext; + +/** + * @author Roy Wetherall + */ +public class BaseRMWebScriptTestCase extends BaseWebScriptTest +{ + /** Site id */ + protected static final String SITE_ID = "mySite"; + + /** Common test utils */ + protected CommonRMTestUtils utils; + + /** Services */ + protected NodeService nodeService; + protected ContentService contentService; + protected DictionaryService dictionaryService; + protected RetryingTransactionHelper retryingTransactionHelper; + protected PolicyComponent policyComponent; + protected NamespaceService namespaceService; + protected SearchService searchService; + protected SiteService siteService; + protected MutableAuthenticationService authenticationService; + protected AuthorityService authorityService; + protected PersonService personService; + + /** RM Services */ + protected RecordsManagementService rmService; + protected DispositionService dispositionService; + protected RecordsManagementEventService eventService; + protected RecordsManagementAdminService adminService; + protected RecordsManagementActionService actionService; + protected RecordsManagementSearchService rmSearchService; + protected RecordsManagementSecurityService securityService; + protected RecordsManagementAuditService auditService; + protected CapabilityService capabilityService; + protected VitalRecordService vitalRecordService; + + /** test data */ + protected StoreRef storeRef; + protected NodeRef rootNodeRef; + protected SiteInfo siteInfo; + protected NodeRef folder; + protected NodeRef filePlan; + protected NodeRef recordSeries; // A category with no disposition schedule + protected NodeRef recordCategory; + protected DispositionSchedule dispositionSchedule; + protected NodeRef recordFolder; + protected NodeRef recordFolder2; + + @Override + protected void setUp() throws Exception + { + super.setUp(); + + // Initialise the service beans + initServices(); + + // Setup test data + setupTestData(); + } + + /** + * Initialise the service beans. + */ + protected void initServices() + { + ApplicationContext applicationContext = getServer().getApplicationContext(); + + // Common test utils + utils = new CommonRMTestUtils(applicationContext); + + // Get services + nodeService = (NodeService)applicationContext.getBean("NodeService"); + contentService = (ContentService)applicationContext.getBean("ContentService"); + retryingTransactionHelper = (RetryingTransactionHelper)applicationContext.getBean("retryingTransactionHelper"); + namespaceService = (NamespaceService)applicationContext.getBean("NamespaceService"); + searchService = (SearchService)applicationContext.getBean("SearchService"); + policyComponent = (PolicyComponent)applicationContext.getBean("policyComponent"); + dictionaryService = (DictionaryService)applicationContext.getBean("DictionaryService"); + siteService = (SiteService)applicationContext.getBean("SiteService"); + authorityService = (AuthorityService)applicationContext.getBean("AuthorityService"); + authenticationService = (MutableAuthenticationService)applicationContext.getBean("AuthenticationService"); + personService = (PersonService)applicationContext.getBean("PersonService"); + + // Get RM services + rmService = (RecordsManagementService)applicationContext.getBean("RecordsManagementService"); + dispositionService = (DispositionService)applicationContext.getBean("DispositionService"); + eventService = (RecordsManagementEventService)applicationContext.getBean("RecordsManagementEventService"); + adminService = (RecordsManagementAdminService)applicationContext.getBean("RecordsManagementAdminService"); + actionService = (RecordsManagementActionService)applicationContext.getBean("RecordsManagementActionService"); + rmSearchService = (RecordsManagementSearchService)applicationContext.getBean("RecordsManagementSearchService"); + securityService = (RecordsManagementSecurityService)applicationContext.getBean("RecordsManagementSecurityService"); + auditService = (RecordsManagementAuditService)applicationContext.getBean("RecordsManagementAuditService"); + capabilityService = (CapabilityService)applicationContext.getBean("CapabilityService"); + vitalRecordService = (VitalRecordService)applicationContext.getBean("VitalRecordService"); + } + + /** + * @see junit.framework.TestCase#tearDown() + */ + @Override + protected void tearDown() throws Exception + { + retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + // As system user + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + + // Do the tear down + tearDownImpl(); + + return null; + } + }); + } + + /** + * Tear down implementation + */ + protected void tearDownImpl() + { + // Delete the folder + nodeService.deleteNode(folder); + + // Delete the site + siteService.deleteSite(SITE_ID); + } + + /** + * Setup test data for tests + */ + protected void setupTestData() + { + retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + setupTestDataImpl(); + return null; + } + }); + } + + /** + * Impl of test data setup + */ + protected void setupTestDataImpl() + { + storeRef = StoreRef.STORE_REF_WORKSPACE_SPACESSTORE; + rootNodeRef = nodeService.getRootNode(storeRef); + + // Create folder + String containerName = "RM2_" + System.currentTimeMillis(); + Map containerProps = new HashMap(1); + containerProps.put(ContentModel.PROP_NAME, containerName); + folder = nodeService.createNode( + rootNodeRef, + ContentModel.ASSOC_CHILDREN, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, containerName), + ContentModel.TYPE_FOLDER, + containerProps).getChildRef(); + assertNotNull("Could not create base folder", folder); + + // Create the site + siteInfo = siteService.createSite("preset", SITE_ID, "title", "descrition", SiteVisibility.PUBLIC, RecordsManagementModel.TYPE_RM_SITE); + filePlan = siteService.getContainer(SITE_ID, RmSiteType.COMPONENT_DOCUMENT_LIBRARY); + assertNotNull("Site document library container was not created successfully.", filePlan); + + recordSeries = rmService.createRecordCategory(filePlan, "recordSeries"); + assertNotNull("Could not create record category with no disposition schedule", recordSeries); + + recordCategory = rmService.createRecordCategory(recordSeries, "rmContainer"); + assertNotNull("Could not create record category", recordCategory); + + // Make vital record + vitalRecordService.setVitalRecordDefintion(recordCategory, true, new Period("week|1")); + + // Create disposition schedule + dispositionSchedule = utils.createBasicDispositionSchedule(recordCategory); + + // Create RM folder + recordFolder = rmService.createRecordFolder(recordCategory, "rmFolder"); + assertNotNull("Could not create rm folder", recordFolder); + recordFolder2 = rmService.createRecordFolder(recordCategory, "rmFolder2"); + assertNotNull("Could not create rm folder 2", recordFolder2); + } +} diff --git a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/util/CommonRMTestUtils.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/util/CommonRMTestUtils.java new file mode 100644 index 0000000000..e146b08575 --- /dev/null +++ b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/util/CommonRMTestUtils.java @@ -0,0 +1,202 @@ +/** + * + */ +package org.alfresco.module.org_alfresco_module_rm.test.util; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService; +import org.alfresco.module.org_alfresco_module_rm.action.impl.FreezeAction; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService; +import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +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; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.springframework.context.ApplicationContext; + +/** + * @author Roy Wetherall + */ +public class CommonRMTestUtils implements RecordsManagementModel +{ + private DispositionService dispositionService; + private NodeService nodeService; + private ContentService contentService; + private RecordsManagementActionService actionService; + + /** test values */ + public static final String DEFAULT_DISPOSITION_AUTHORITY = "disposition authority"; + public static final String DEFAULT_DISPOSITION_INSTRUCTIONS = "disposition instructions"; + public static final String DEFAULT_DISPOSITION_DESCRIPTION = "disposition action description"; + public static final String DEFAULT_EVENT_NAME = "case_closed"; + public static final String PERIOD_NONE = "none|0"; + public static final String PERIOD_IMMEDIATELY = "immediately|0"; + + public CommonRMTestUtils(ApplicationContext applicationContext) + { + dispositionService = (DispositionService)applicationContext.getBean("DispositionService"); + nodeService = (NodeService)applicationContext.getBean("NodeService"); + contentService = (ContentService)applicationContext.getBean("ContentService"); + actionService = (RecordsManagementActionService)applicationContext.getBean("RecordsManagementActionService"); + } + + /** + * + * @param container + * @return + */ + public DispositionSchedule createBasicDispositionSchedule(NodeRef container) + { + return createBasicDispositionSchedule(container, DEFAULT_DISPOSITION_INSTRUCTIONS, DEFAULT_DISPOSITION_AUTHORITY, false, true); + } + + /** + * + * @param container + * @param isRecordLevel + * @param defaultDispositionActions + * @return + */ + public DispositionSchedule createBasicDispositionSchedule( + NodeRef container, + String dispositionInstructions, + String dispositionAuthority, + boolean isRecordLevel, + boolean defaultDispositionActions) + { + Map dsProps = new HashMap(3); + dsProps.put(PROP_DISPOSITION_AUTHORITY, dispositionAuthority); + dsProps.put(PROP_DISPOSITION_INSTRUCTIONS, dispositionInstructions); + dsProps.put(PROP_RECORD_LEVEL_DISPOSITION, isRecordLevel); + DispositionSchedule dispositionSchedule = dispositionService.createDispositionSchedule(container, dsProps); + + if (defaultDispositionActions == true) + { + Map adParams = new HashMap(3); + adParams.put(PROP_DISPOSITION_ACTION_NAME, "cutoff"); + adParams.put(PROP_DISPOSITION_DESCRIPTION, DEFAULT_DISPOSITION_DESCRIPTION); + + List events = new ArrayList(1); + events.add(DEFAULT_EVENT_NAME); + adParams.put(PROP_DISPOSITION_EVENT, (Serializable)events); + + dispositionService.addDispositionActionDefinition(dispositionSchedule, adParams); + + adParams = new HashMap(3); + adParams.put(PROP_DISPOSITION_ACTION_NAME, "destroy"); + adParams.put(PROP_DISPOSITION_DESCRIPTION, DEFAULT_DISPOSITION_DESCRIPTION); + adParams.put(PROP_DISPOSITION_PERIOD, "immediately|0"); + + dispositionService.addDispositionActionDefinition(dispositionSchedule, adParams); + } + + return dispositionSchedule; + } + + public NodeRef createRecord(NodeRef recordFolder, String name) + { + return createRecord(recordFolder, name, null, "Some test content"); + } + + public NodeRef createRecord(NodeRef recordFolder, String name, String title) + { + Map props = new HashMap(1); + props.put(ContentModel.PROP_TITLE, title); + return createRecord(recordFolder, name, props, "Some test content"); + } + + public NodeRef createRecord(NodeRef recordFolder, String name, Map properties, String content) + { + // Create the document + if (properties == null) + { + properties = new HashMap(1); + } + if (properties.containsKey(ContentModel.PROP_NAME) == false) + { + properties.put(ContentModel.PROP_NAME, name); + } + NodeRef recordOne = nodeService.createNode(recordFolder, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, name), + ContentModel.TYPE_CONTENT, + properties).getChildRef(); + + // Set the content + ContentWriter writer = contentService.getWriter(recordOne, ContentModel.PROP_CONTENT, true); + writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.setEncoding("UTF-8"); + writer.putContent(content); + + return recordOne; + } + + public void declareRecord(final NodeRef record) + { + AuthenticationUtil.runAs(new RunAsWork() + { + @Override + public Void doWork() throws Exception + { + // Declare record + nodeService.setProperty(record, RecordsManagementModel.PROP_PUBLICATION_DATE, new Date()); + nodeService.setProperty(record, RecordsManagementModel.PROP_MEDIA_TYPE, "mediaTypeValue"); + nodeService.setProperty(record, RecordsManagementModel.PROP_FORMAT, "formatValue"); + nodeService.setProperty(record, RecordsManagementModel.PROP_DATE_RECEIVED, new Date()); + nodeService.setProperty(record, RecordsManagementModel.PROP_DATE_FILED, new Date()); + nodeService.setProperty(record, RecordsManagementModel.PROP_ORIGINATOR, "origValue"); + nodeService.setProperty(record, RecordsManagementModel.PROP_ORIGINATING_ORGANIZATION, "origOrgValue"); + nodeService.setProperty(record, ContentModel.PROP_TITLE, "titleValue"); + actionService.executeRecordsManagementAction(record, "declareRecord"); + + return null; + } + + }, AuthenticationUtil.getAdminUserName()); + + } + + public void freeze(final NodeRef nodeRef) + { + AuthenticationUtil.runAs(new RunAsWork() + { + @Override + public Void doWork() throws Exception + { + Map params = new HashMap(1); + params.put(FreezeAction.PARAM_REASON, "Freeze reason."); + actionService.executeRecordsManagementAction(nodeRef, "freeze", params); + + return null; + } + + }, AuthenticationUtil.getSystemUserName()); + } + + public void unfreeze(final NodeRef nodeRef) + { + AuthenticationUtil.runAs(new RunAsWork() + { + @Override + public Void doWork() throws Exception + { + actionService.executeRecordsManagementAction(nodeRef, "unfreeze"); + return null; + } + + }, AuthenticationUtil.getSystemUserName()); + } +} diff --git a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestUtilities.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestUtilities.java index 5a1f68f198..214e03d4a9 100644 --- a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestUtilities.java +++ b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/util/TestUtilities.java @@ -24,6 +24,7 @@ import java.io.Reader; import java.io.Serializable; import java.util.ArrayList; import java.util.Date; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -33,7 +34,6 @@ import org.alfresco.model.ContentModel; import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService; import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService; -import org.alfresco.module.org_alfresco_module_rm.dod5015.DOD5015Model; import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementSearchBehaviour; import org.alfresco.module.org_alfresco_module_rm.script.BootstrapTestDataGet; @@ -42,8 +42,6 @@ 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.cmr.search.ResultSet; -import org.alfresco.service.cmr.search.SearchParameters; import org.alfresco.service.cmr.search.SearchService; import org.alfresco.service.cmr.security.AuthorityService; import org.alfresco.service.cmr.security.PermissionService; @@ -51,7 +49,6 @@ import org.alfresco.service.cmr.view.ImporterBinding; import org.alfresco.service.cmr.view.ImporterService; import org.alfresco.service.cmr.view.Location; import org.alfresco.service.namespace.QName; -import org.alfresco.util.ISO9075; import org.springframework.context.ApplicationContext; /** @@ -67,6 +64,22 @@ public class TestUtilities implements RecordsManagementModel return TestUtilities.loadFilePlanData(applicationContext, true, false); } + public static final String TEST_FILE_PLAN_NAME = "testUtilities.filePlan"; + + private static NodeRef getFilePlan(NodeService nodeService, NodeRef rootNode) + { + NodeRef filePlan = null; + + // Try and find a file plan hanging from the root node + List assocs = nodeService.getChildAssocs(rootNode, ContentModel.ASSOC_CHILDREN, TYPE_FILE_PLAN); + if (assocs.size() != 0) + { + filePlan = assocs.get(0).getChildRef(); + } + + return filePlan; + } + public static NodeRef loadFilePlanData(ApplicationContext applicationContext, boolean patchData, boolean alwaysLoad) { NodeService nodeService = (NodeService)applicationContext.getBean("NodeService"); @@ -83,21 +96,18 @@ public class TestUtilities implements RecordsManagementModel NodeRef filePlan = null; NodeRef rootNode = nodeService.getRootNode(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE); - if (alwaysLoad == false) + if (alwaysLoad == false && getFilePlan(nodeService, rootNode) != null) { - // Try and find a file plan hanging from the root node - List assocs = nodeService.getChildAssocs(rootNode, ContentModel.ASSOC_CHILDREN, TYPE_FILE_PLAN); - if (assocs.size() != 0) - { - filePlan = assocs.get(0).getChildRef(); - return filePlan; - } + return filePlan; } // For now creating the filePlan beneath the + Map props = new HashMap(1); + props.put(ContentModel.PROP_NAME, TEST_FILE_PLAN_NAME); filePlan = nodeService.createNode(rootNode, ContentModel.ASSOC_CHILDREN, TYPE_FILE_PLAN, - TYPE_FILE_PLAN).getChildRef(); + TYPE_FILE_PLAN, + props).getChildRef(); // Do the data load into the the provided filePlan node reference // TODO ... @@ -122,71 +132,42 @@ public class TestUtilities implements RecordsManagementModel return filePlan; } - public static NodeRef getRecordSeries(SearchService searchService, String seriesName) + public static NodeRef getRecordSeries(RecordsManagementService rmService, NodeService nodeService, String seriesName) { - SearchParameters searchParameters = new SearchParameters(); - searchParameters.addStore(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE); - - String query = "PATH:\"dod:filePlan/cm:" + ISO9075.encode(seriesName) + "\""; - - searchParameters.setQuery(query); - searchParameters.setLanguage(SearchService.LANGUAGE_LUCENE); - ResultSet rs = searchService.query(searchParameters); - try - { - //setComplete(); - //endTransaction(); - return rs.getNodeRefs().isEmpty() ? null : rs.getNodeRef(0); - } - finally - { - rs.close(); - } + NodeRef result = null; + NodeRef rootNode = nodeService.getRootNode(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE); + NodeRef filePlan = getFilePlan(nodeService, rootNode); + + if (filePlan != null) + { + result = nodeService.getChildByName(filePlan, ContentModel.ASSOC_CONTAINS, seriesName); + } + return result; } - public static NodeRef getRecordCategory(SearchService searchService, String seriesName, String categoryName) + public static NodeRef getRecordCategory(RecordsManagementService rmService, NodeService nodeService, String seriesName, String categoryName) { - SearchParameters searchParameters = new SearchParameters(); - searchParameters.addStore(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE); - - String query = "PATH:\"dod:filePlan/cm:" + ISO9075.encode(seriesName) + "/cm:" + ISO9075.encode(categoryName) + "\""; - - searchParameters.setQuery(query); - searchParameters.setLanguage(SearchService.LANGUAGE_LUCENE); - ResultSet rs = searchService.query(searchParameters); - try - { - //setComplete(); - //endTransaction(); - return rs.getNodeRefs().isEmpty() ? null : rs.getNodeRef(0); - } - finally - { - rs.close(); - } + NodeRef seriesNodeRef = getRecordSeries(rmService, nodeService, seriesName); + + NodeRef result = null; + if (seriesNodeRef != null) + { + result = nodeService.getChildByName(seriesNodeRef, ContentModel.ASSOC_CONTAINS, categoryName); + } + return result; } - public static NodeRef getRecordFolder(SearchService searchService, String seriesName, String categoryName, String folderName) + public static NodeRef getRecordFolder(RecordsManagementService rmService, NodeService nodeService, String seriesName, String categoryName, String folderName) { - SearchParameters searchParameters = new SearchParameters(); - searchParameters.addStore(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE); - String query = "PATH:\"dod:filePlan/cm:" + ISO9075.encode(seriesName) - + "/cm:" + ISO9075.encode(categoryName) - + "/cm:" + ISO9075.encode(folderName) + "\""; - System.out.println("Query: " + query); - searchParameters.setQuery(query); - searchParameters.setLanguage(SearchService.LANGUAGE_LUCENE); - ResultSet rs = searchService.query(searchParameters); - try - { - // setComplete(); - // endTransaction(); - return rs.getNodeRefs().isEmpty() ? null : rs.getNodeRef(0); - } - finally - { - rs.close(); - } + NodeRef categoryNodeRef = getRecordCategory(rmService, nodeService, seriesName, categoryName); + + NodeRef result = null; + if (categoryNodeRef != null) + { + result = nodeService.getChildByName(categoryNodeRef, ContentModel.ASSOC_CONTAINS, folderName); + } + return result; + } diff --git a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/BootstraptestDataRestApiTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/BootstraptestDataRestApiTest.java index 58c486efdd..e137b85263 100644 --- a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/BootstraptestDataRestApiTest.java +++ b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/BootstraptestDataRestApiTest.java @@ -19,6 +19,7 @@ package org.alfresco.module.org_alfresco_module_rm.test.webscript; import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; +import org.alfresco.module.org_alfresco_module_rm.test.util.BaseRMWebScriptTestCase; import org.alfresco.module.org_alfresco_module_rm.test.util.TestUtilities; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.transaction.RetryingTransactionHelper; @@ -33,7 +34,7 @@ import org.springframework.extensions.webscripts.TestWebScriptServer.GetRequest; * * @author Roy Wetherall */ -public class BootstraptestDataRestApiTest extends BaseWebScriptTest implements RecordsManagementModel +public class BootstraptestDataRestApiTest extends BaseRMWebScriptTestCase implements RecordsManagementModel { protected static StoreRef SPACES_STORE = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "SpacesStore"); protected static final String URL = "/api/rma/bootstraptestdata"; diff --git a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/DispositionRestApiTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/DispositionRestApiTest.java index 6795639b56..7a6521d4ed 100644 --- a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/DispositionRestApiTest.java +++ b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/DispositionRestApiTest.java @@ -18,36 +18,15 @@ */ package org.alfresco.module.org_alfresco_module_rm.test.webscript; -import java.io.Serializable; import java.text.MessageFormat; -import java.util.Date; -import java.util.Map; -import javax.transaction.UserTransaction; - -import org.alfresco.model.ContentModel; -import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; -import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService; -import org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEventService; import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; -import org.alfresco.module.org_alfresco_module_rm.test.util.TestUtilities; -import org.alfresco.repo.content.MimetypeMap; -import org.alfresco.repo.security.authentication.AuthenticationUtil; -import org.alfresco.repo.transaction.RetryingTransactionHelper; -import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; -import org.alfresco.repo.web.scripts.BaseWebScriptTest; -import org.alfresco.service.cmr.repository.ContentService; -import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.module.org_alfresco_module_rm.test.util.BaseRMWebScriptTestCase; +import org.alfresco.module.org_alfresco_module_rm.test.util.CommonRMTestUtils; import org.alfresco.service.cmr.repository.NodeRef; -import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.repository.Period; import org.alfresco.service.cmr.repository.StoreRef; -import org.alfresco.service.cmr.search.SearchService; -import org.alfresco.service.cmr.security.PermissionService; -import org.alfresco.service.cmr.view.ImporterService; -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.json.JSONArray; import org.json.JSONObject; import org.json.JSONTokener; @@ -62,7 +41,7 @@ import org.springframework.extensions.webscripts.TestWebScriptServer.Response; * * @author Gavin Cornwell */ -public class DispositionRestApiTest extends BaseWebScriptTest implements RecordsManagementModel +public class DispositionRestApiTest extends BaseRMWebScriptTestCase implements RecordsManagementModel { protected static StoreRef SPACES_STORE = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "SpacesStore"); protected static final String GET_SCHEDULE_URL_FORMAT = "/api/node/{0}/dispositionschedule"; @@ -74,50 +53,6 @@ public class DispositionRestApiTest extends BaseWebScriptTest implements Records protected static final String SERVICE_URL_PREFIX = "/alfresco/service"; protected static final String APPLICATION_JSON = "application/json"; - protected NodeService nodeService; - protected ContentService contentService; - protected SearchService searchService; - protected ImporterService importService; - protected PermissionService permissionService; - protected TransactionService transactionService; - protected RecordsManagementService rmService; - protected RecordsManagementActionService rmActionService; - protected RecordsManagementEventService rmEventService; - protected RetryingTransactionHelper retryingTransactionHelper; - - @Override - protected void setUp() throws Exception - { - super.setUp(); - this.nodeService = (NodeService) getServer().getApplicationContext().getBean("NodeService"); - this.contentService = (ContentService)getServer().getApplicationContext().getBean("ContentService"); - this.searchService = (SearchService)getServer().getApplicationContext().getBean("SearchService"); - this.importService = (ImporterService)getServer().getApplicationContext().getBean("ImporterService"); - this.permissionService = (PermissionService)getServer().getApplicationContext().getBean("PermissionService"); - this.transactionService = (TransactionService)getServer().getApplicationContext().getBean("TransactionService"); - this.rmService = (RecordsManagementService)getServer().getApplicationContext().getBean("RecordsManagementService"); - this.rmActionService = (RecordsManagementActionService)getServer().getApplicationContext().getBean("RecordsManagementActionService"); - this.rmEventService = (RecordsManagementEventService)getServer().getApplicationContext().getBean("RecordsManagementEventService"); - this.retryingTransactionHelper = (RetryingTransactionHelper)getServer().getApplicationContext().getBean("retryingTransactionHelper"); - - AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); - - retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() - { - @Override - public Void execute() throws Throwable - { - TestUtilities.loadFilePlanData(getServer().getApplicationContext()); - return null; - } - }); - - // Bring the filePlan into the test database. - // - // This is quite a slow call, so if this class grew to have many test methods, - // there would be a real benefit in using something like @BeforeClass for the line below. - //TestUtilities.loadFilePlanData(getServer().getApplicationContext()); - } public void testGetDispositionSchedule() throws Exception { @@ -128,20 +63,16 @@ public class DispositionRestApiTest extends BaseWebScriptTest implements Records Response rsp = sendRequest(new GetRequest(nonExistentUrl), expectedStatus); // Test 404 status for node that doesn't have dispostion schedule i.e. a record series - NodeRef series = TestUtilities.getRecordSeries(searchService, "Reports"); - assertNotNull(series); - String seriesNodeUrl = series.toString().replace("://", "/"); + String seriesNodeUrl = recordSeries.toString().replace("://", "/"); String wrongNodeUrl = MessageFormat.format(GET_SCHEDULE_URL_FORMAT, seriesNodeUrl); rsp = sendRequest(new GetRequest(wrongNodeUrl), expectedStatus); // Test data structure returned from "AIS Audit Records" expectedStatus = 200; - NodeRef recordCategory = TestUtilities.getRecordCategory(this.searchService, "Reports", "AIS Audit Records"); - assertNotNull(recordCategory); + String categoryNodeUrl = recordCategory.toString().replace("://", "/"); String requestUrl = MessageFormat.format(GET_SCHEDULE_URL_FORMAT, categoryNodeUrl); rsp = sendRequest(new GetRequest(requestUrl), expectedStatus); - System.out.println(" 888 GET response: " + rsp.getContentAsString()); assertEquals("application/json;charset=UTF-8", rsp.getContentType()); // get response as JSON @@ -160,10 +91,11 @@ public class DispositionRestApiTest extends BaseWebScriptTest implements Records assertEquals(serviceUrl, url); String authority = rootDataObject.getString("authority"); - assertEquals("N1-218-00-4 item 023", authority); + + assertEquals(CommonRMTestUtils.DEFAULT_DISPOSITION_AUTHORITY, authority); String instructions = rootDataObject.getString("instructions"); - assertEquals("Cut off monthly, hold 1 month, then destroy.", instructions); + assertEquals(CommonRMTestUtils.DEFAULT_DISPOSITION_INSTRUCTIONS, instructions); String actionsUrl = rootDataObject.getString("actionsUrl"); assertEquals(serviceUrl + "/dispositionactiondefinitions", actionsUrl); @@ -177,84 +109,26 @@ public class DispositionRestApiTest extends BaseWebScriptTest implements Records assertNotNull(actions); assertEquals(2, actions.length()); JSONObject action1 = (JSONObject)actions.get(0); - assertEquals(7, action1.length()); + assertEquals(9, action1.length()); assertNotNull(action1.get("id")); assertNotNull(action1.get("url")); assertEquals(0, action1.getInt("index")); assertEquals("cutoff", action1.getString("name")); assertEquals("Cutoff", action1.getString("label")); - assertEquals("monthend|1", action1.getString("period")); assertTrue(action1.getBoolean("eligibleOnFirstCompleteEvent")); JSONObject action2 = (JSONObject)actions.get(1); assertEquals(8, action2.length()); - assertEquals("rma:cutOffDate", action2.get("periodProperty")); // make sure the disposition schedule node ref is present and valid String scheduleNodeRefJSON = rootDataObject.getString("nodeRef"); NodeRef scheduleNodeRef = new NodeRef(scheduleNodeRefJSON); assertTrue(this.nodeService.exists(scheduleNodeRef)); - // Test data structure returned from "Personnel Security Program Records" - recordCategory = TestUtilities.getRecordCategory(this.searchService, "Civilian Files", "Employee Performance File System Records"); - assertNotNull(recordCategory); - categoryNodeUrl = recordCategory.toString().replace("://", "/"); - requestUrl = MessageFormat.format(GET_SCHEDULE_URL_FORMAT, categoryNodeUrl); - rsp = sendRequest(new GetRequest(requestUrl), expectedStatus); - //System.out.println("GET response: " + rsp.getContentAsString()); - assertEquals("application/json;charset=UTF-8", rsp.getContentType()); - - // get response as JSON - jsonParsedObject = new JSONObject(new JSONTokener(rsp.getContentAsString())); - assertNotNull(jsonParsedObject); - - // check JSON data - dataObj = jsonParsedObject.getJSONObject("data"); - assertNotNull(dataObj); - rootDataObject = (JSONObject)dataObj; - assertEquals(10, rootDataObject.length()); - - // check individual data items - serviceUrl = SERVICE_URL_PREFIX + requestUrl; - url = rootDataObject.getString("url"); - assertEquals(serviceUrl, url); - - authority = rootDataObject.getString("authority"); - assertEquals("GRS 1 item 23b(1)", authority); - - instructions = rootDataObject.getString("instructions"); - assertEquals("Cutoff when superseded. Destroy immediately after cutoff", instructions); - - recordLevel = rootDataObject.getBoolean("recordLevelDisposition"); - assertTrue(recordLevel); - - assertTrue(rootDataObject.getBoolean("canStepsBeRemoved")); - - actions = rootDataObject.getJSONArray("actions"); - assertNotNull(actions); - assertEquals(2, actions.length()); - action1 = (JSONObject)actions.get(0); - assertEquals(8, action1.length()); - assertNotNull(action1.get("id")); - assertNotNull(action1.get("url")); - assertEquals(0, action1.getInt("index")); - assertEquals("cutoff", action1.getString("name")); - assertEquals("Cutoff", action1.getString("label")); - assertTrue(action1.getBoolean("eligibleOnFirstCompleteEvent")); - JSONArray events = action1.getJSONArray("events"); - assertNotNull(events); - assertEquals(1, events.length()); - assertEquals("superseded", events.get(0)); - - // Test the retrieval of an empty disposition schedule - NodeRef recordSeries = TestUtilities.getRecordSeries(this.searchService, "Civilian Files"); - assertNotNull(recordSeries); - // create a new recordCategory node in the recordSeries and then get // the disposition schedule - NodeRef newRecordCategory = this.nodeService.createNode(recordSeries, ContentModel.ASSOC_CONTAINS, - QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, QName.createValidLocalName("recordCategory")), - TYPE_RECORD_CATEGORY).getChildRef(); + NodeRef newRecordCategory = rmService.createRecordCategory(recordSeries, GUID.generate()); + dispositionService.createDispositionSchedule(newRecordCategory, null); categoryNodeUrl = newRecordCategory.toString().replace("://", "/"); requestUrl = MessageFormat.format(GET_SCHEDULE_URL_FORMAT, categoryNodeUrl); @@ -278,15 +152,10 @@ public class DispositionRestApiTest extends BaseWebScriptTest implements Records public void testPostDispositionAction() throws Exception { - // create a recordCategory to get a disposition schedule - NodeRef recordSeries = TestUtilities.getRecordSeries(this.searchService, "Civilian Files"); - assertNotNull(recordSeries); - // create a new recordCategory node in the recordSeries and then get // the disposition schedule - NodeRef newRecordCategory = this.nodeService.createNode(recordSeries, ContentModel.ASSOC_CONTAINS, - QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, QName.createValidLocalName("recordCategory")), - TYPE_RECORD_CATEGORY).getChildRef(); + NodeRef newRecordCategory = rmService.createRecordCategory(recordSeries, GUID.generate()); + dispositionService.createDispositionSchedule(newRecordCategory, null); String categoryNodeUrl = newRecordCategory.toString().replace("://", "/"); String requestUrl = MessageFormat.format(POST_ACTIONDEF_URL_FORMAT, categoryNodeUrl); @@ -363,13 +232,8 @@ public class DispositionRestApiTest extends BaseWebScriptTest implements Records public void testPutDispositionAction() throws Exception { - // create a new recordCategory node in the recordSeries and then get - // the disposition schedule - NodeRef recordSeries = TestUtilities.getRecordSeries(this.searchService, "Civilian Files"); - assertNotNull(recordSeries); - NodeRef newRecordCategory = this.nodeService.createNode(recordSeries, ContentModel.ASSOC_CONTAINS, - QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, QName.createValidLocalName("recordCategory")), - TYPE_RECORD_CATEGORY).getChildRef(); + NodeRef newRecordCategory = rmService.createRecordCategory(recordSeries, GUID.generate()); + dispositionService.createDispositionSchedule(newRecordCategory, null); // create an action definition to then update String categoryNodeUrl = newRecordCategory.toString().replace("://", "/"); @@ -437,13 +301,8 @@ public class DispositionRestApiTest extends BaseWebScriptTest implements Records public void testDeleteDispositionAction() throws Exception { - // create a new recordCategory node in the recordSeries and then get - // the disposition schedule - NodeRef recordSeries = TestUtilities.getRecordSeries(this.searchService, "Civilian Files"); - assertNotNull(recordSeries); - NodeRef newRecordCategory = this.nodeService.createNode(recordSeries, ContentModel.ASSOC_CONTAINS, - QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, QName.createValidLocalName("recordCategory")), - TYPE_RECORD_CATEGORY).getChildRef(); + NodeRef newRecordCategory = rmService.createRecordCategory(recordSeries, GUID.generate()); + dispositionService.createDispositionSchedule(newRecordCategory, null); // create an action definition to then delete String categoryNodeUrl = newRecordCategory.toString().replace("://", "/"); @@ -477,51 +336,18 @@ public class DispositionRestApiTest extends BaseWebScriptTest implements Records public void testGetDispositionLifecycle() throws Exception { - // create a new recordFolder in a recordCategory - NodeRef recordCategory = TestUtilities.getRecordCategory(this.searchService, "Military Files", - "Military Assignment Documents"); - assertNotNull(recordCategory); - // Test 404 for disposition lifecycle request on incorrect node String categoryUrl = recordCategory.toString().replace("://", "/"); String requestUrl = MessageFormat.format(GET_LIFECYCLE_URL_FORMAT, categoryUrl); Response rsp = sendRequest(new GetRequest(requestUrl), 404); - UserTransaction txn = transactionService.getUserTransaction(false); - txn.begin(); - - NodeRef newRecordFolder = this.nodeService.createNode(recordCategory, ContentModel.ASSOC_CONTAINS, - QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, QName.createValidLocalName("recordFolder")), - TYPE_RECORD_FOLDER).getChildRef(); - - txn.commit(); - txn = transactionService.getUserTransaction(false); - txn.begin(); - - // Create the document - NodeRef recordOne = this.nodeService.createNode(newRecordFolder, - ContentModel.ASSOC_CONTAINS, - QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "record"), - ContentModel.TYPE_CONTENT).getChildRef(); - - // Set the content - ContentWriter writer = this.contentService.getWriter(recordOne, ContentModel.PROP_CONTENT, true); - writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); - writer.setEncoding("UTF-8"); - writer.putContent("There is some content in this record"); - - txn.commit(); // - triggers FileAction - - txn = transactionService.getUserTransaction(false); - txn.begin(); - declareRecord(recordOne); - txn.commit(); + NodeRef newRecordFolder = rmService.createRecordFolder(recordCategory, "recordFolder"); + // there should now be a disposition lifecycle for the record - String recordUrl = recordOne.toString().replace("://", "/"); - requestUrl = MessageFormat.format(GET_LIFECYCLE_URL_FORMAT, recordUrl); + requestUrl = MessageFormat.format(GET_LIFECYCLE_URL_FORMAT, newRecordFolder.toString().replace("://", "/")); rsp = sendRequest(new GetRequest(requestUrl), 200); - //System.out.println("GET : " + rsp.getContentAsString()); + System.out.println("GET : " + rsp.getContentAsString()); assertEquals("application/json;charset=UTF-8", rsp.getContentType()); // get response as JSON @@ -538,10 +364,10 @@ public class DispositionRestApiTest extends BaseWebScriptTest implements Records JSONArray events = dataObj.getJSONArray("events"); assertEquals(1, events.length()); JSONObject event1 = events.getJSONObject(0); - assertEquals("superseded", event1.get("name")); - assertEquals("Superseded", event1.get("label")); + assertEquals("case_closed", event1.get("name")); + assertEquals("Case Closed", event1.get("label")); assertFalse(event1.getBoolean("complete")); - assertTrue(event1.getBoolean("automatic")); + assertFalse(event1.getBoolean("automatic")); // check stuff expected to be missing is missing assertFalse(dataObj.has("asOf")); @@ -556,10 +382,8 @@ public class DispositionRestApiTest extends BaseWebScriptTest implements Records public void testGetListOfValues() throws Exception { // call the list service - Response rsp = sendRequest(new GetRequest(GET_LIST_URL), 200); - //System.out.println("GET : " + rsp.getContentAsString()); + Response rsp = sendRequest(new GetRequest(GET_LIST_URL), 200); assertEquals("application/json;charset=UTF-8", rsp.getContentType()); - //System.out.println(rsp.getContentAsString()); // get response as JSON JSONObject jsonParsedObject = new JSONObject(new JSONTokener(rsp.getContentAsString())); @@ -570,7 +394,7 @@ public class DispositionRestApiTest extends BaseWebScriptTest implements Records JSONObject actions = data.getJSONObject("dispositionActions"); assertEquals(SERVICE_URL_PREFIX + GET_LIST_URL + "/dispositionactions", actions.getString("url")); JSONArray items = actions.getJSONArray("items"); - assertEquals(this.rmActionService.getDispositionActions().size(), items.length()); + assertEquals(actionService.getDispositionActions().size(), items.length()); assertTrue(items.length() > 0); JSONObject item = items.getJSONObject(0); assertTrue(item.length() == 2); @@ -581,7 +405,7 @@ public class DispositionRestApiTest extends BaseWebScriptTest implements Records JSONObject events = data.getJSONObject("events"); assertEquals(SERVICE_URL_PREFIX + GET_LIST_URL + "/events", events.getString("url")); items = events.getJSONArray("items"); - assertEquals(this.rmEventService.getEvents().size(), items.length()); + assertEquals(eventService.getEvents().size(), items.length()); assertTrue(items.length() > 0); item = items.getJSONObject(0); assertTrue(item.length() == 3); @@ -611,23 +435,4 @@ public class DispositionRestApiTest extends BaseWebScriptTest implements Records assertTrue(item.has("label")); assertTrue(item.has("value")); } - - private void declareRecord(NodeRef recordOne) - { - // Declare record - Map propValues = this.nodeService.getProperties(recordOne); - propValues.put(RecordsManagementModel.PROP_PUBLICATION_DATE, new Date()); - // List smList = new ArrayList(2); - // smList.add("FOUO"); - // smList.add("NOFORN"); - // propValues.put(RecordsManagementModel.PROP_SUPPLEMENTAL_MARKING_LIST, (Serializable)smList); - propValues.put(RecordsManagementModel.PROP_MEDIA_TYPE, "mediaTypeValue"); - propValues.put(RecordsManagementModel.PROP_FORMAT, "formatValue"); - propValues.put(RecordsManagementModel.PROP_DATE_RECEIVED, new Date()); - propValues.put(RecordsManagementModel.PROP_ORIGINATOR, "origValue"); - propValues.put(RecordsManagementModel.PROP_ORIGINATING_ORGANIZATION, "origOrgValue"); - propValues.put(ContentModel.PROP_TITLE, "titleValue"); - this.nodeService.setProperties(recordOne, propValues); - this.rmActionService.executeRecordsManagementAction(recordOne, "declareRecord"); - } } diff --git a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/EmailMapScriptTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/EmailMapScriptTest.java index 90c0a574f8..1187aebf9b 100644 --- a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/EmailMapScriptTest.java +++ b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/EmailMapScriptTest.java @@ -18,8 +18,8 @@ */ package org.alfresco.module.org_alfresco_module_rm.test.webscript; +import org.alfresco.module.org_alfresco_module_rm.test.util.BaseRMWebScriptTestCase; import org.alfresco.repo.security.authentication.AuthenticationUtil; -import org.alfresco.repo.web.scripts.BaseWebScriptTest; import org.alfresco.service.cmr.security.AuthenticationService; import org.json.JSONArray; import org.json.JSONObject; @@ -28,7 +28,7 @@ import org.springframework.extensions.webscripts.TestWebScriptServer.GetRequest; import org.springframework.extensions.webscripts.TestWebScriptServer.PostRequest; import org.springframework.extensions.webscripts.TestWebScriptServer.Response; -public class EmailMapScriptTest extends BaseWebScriptTest +public class EmailMapScriptTest extends BaseRMWebScriptTestCase { public final static String URL_RM_EMAILMAP = "/api/rma/admin/emailmap"; diff --git a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/EventRestApiTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/EventRestApiTest.java index c1b1aa5a66..8b8229278c 100644 --- a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/EventRestApiTest.java +++ b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/EventRestApiTest.java @@ -18,54 +18,34 @@ */ package org.alfresco.module.org_alfresco_module_rm.test.webscript; -import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; -import org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEventService; import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; -import org.alfresco.repo.security.authentication.AuthenticationUtil; -import org.alfresco.repo.web.scripts.BaseWebScriptTest; -import org.alfresco.service.cmr.repository.NodeService; -import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.module.org_alfresco_module_rm.test.util.BaseRMWebScriptTestCase; import org.alfresco.util.GUID; +import org.json.JSONObject; import org.springframework.extensions.webscripts.TestWebScriptServer.DeleteRequest; import org.springframework.extensions.webscripts.TestWebScriptServer.GetRequest; import org.springframework.extensions.webscripts.TestWebScriptServer.PostRequest; import org.springframework.extensions.webscripts.TestWebScriptServer.PutRequest; import org.springframework.extensions.webscripts.TestWebScriptServer.Response; -import org.json.JSONObject; /** * RM event REST API test * * @author Roy Wetherall */ -public class EventRestApiTest extends BaseWebScriptTest implements RecordsManagementModel +public class EventRestApiTest extends BaseRMWebScriptTestCase implements RecordsManagementModel { - protected static StoreRef SPACES_STORE = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "SpacesStore"); protected static final String GET_EVENTS_URL = "/api/rma/admin/rmevents"; protected static final String GET_EVENTTYPES_URL = "/api/rma/admin/rmeventtypes"; protected static final String SERVICE_URL_PREFIX = "/alfresco/service"; protected static final String APPLICATION_JSON = "application/json"; + protected static final String DISPLAY_LABEL = "display label"; protected static final String EVENT_TYPE = "rmEventType.simple"; protected static final String KEY_EVENT_NAME = "eventName"; protected static final String KEY_EVENT_TYPE = "eventType"; protected static final String KEY_EVENT_DISPLAY_LABEL = "eventDisplayLabel"; - - protected NodeService nodeService; - protected RecordsManagementService rmService; - protected RecordsManagementEventService rmEventService; - - @Override - protected void setUp() throws Exception - { - super.setUp(); - this.nodeService = (NodeService) getServer().getApplicationContext().getBean("NodeService"); - this.rmService = (RecordsManagementService)getServer().getApplicationContext().getBean("RecordsManagementService"); - this.rmEventService = (RecordsManagementEventService)getServer().getApplicationContext().getBean("RecordsManagementEventService"); - - AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); - } - + public void testGetEventTypes() throws Exception { Response rsp = sendRequest(new GetRequest(GET_EVENTTYPES_URL),200); @@ -89,8 +69,8 @@ public class EventRestApiTest extends BaseWebScriptTest implements RecordsManage String event2 = GUID.generate(); // Create a couple or events by hand - rmEventService.addEvent(EVENT_TYPE, event1, DISPLAY_LABEL); - rmEventService.addEvent(EVENT_TYPE, event2, DISPLAY_LABEL); + eventService.addEvent(EVENT_TYPE, event1, DISPLAY_LABEL); + eventService.addEvent(EVENT_TYPE, event2, DISPLAY_LABEL); try { @@ -117,8 +97,8 @@ public class EventRestApiTest extends BaseWebScriptTest implements RecordsManage finally { // Clean up - rmEventService.removeEvent(event1); - rmEventService.removeEvent(event2); + eventService.removeEvent(event1); + eventService.removeEvent(event2); } } @@ -148,7 +128,7 @@ public class EventRestApiTest extends BaseWebScriptTest implements RecordsManage } finally { - rmEventService.removeEvent(eventName); + eventService.removeEvent(eventName); } // Test with no event name set @@ -172,14 +152,14 @@ public class EventRestApiTest extends BaseWebScriptTest implements RecordsManage } finally { - rmEventService.removeEvent(eventName); + eventService.removeEvent(eventName); } } public void testPutRole() throws Exception { String eventName = GUID.generate(); - rmEventService.addEvent(EVENT_TYPE, eventName, DISPLAY_LABEL); + eventService.addEvent(EVENT_TYPE, eventName, DISPLAY_LABEL); try { @@ -206,7 +186,7 @@ public class EventRestApiTest extends BaseWebScriptTest implements RecordsManage finally { // Clean up - rmEventService.removeEvent(eventName); + eventService.removeEvent(eventName); } } @@ -214,7 +194,7 @@ public class EventRestApiTest extends BaseWebScriptTest implements RecordsManage public void testGetRole() throws Exception { String eventName = GUID.generate(); - rmEventService.addEvent(EVENT_TYPE, eventName, DISPLAY_LABEL); + eventService.addEvent(EVENT_TYPE, eventName, DISPLAY_LABEL); try { @@ -236,7 +216,7 @@ public class EventRestApiTest extends BaseWebScriptTest implements RecordsManage finally { // Clean up - rmEventService.removeEvent(eventName); + eventService.removeEvent(eventName); } } @@ -244,11 +224,11 @@ public class EventRestApiTest extends BaseWebScriptTest implements RecordsManage public void testDeleteRole() throws Exception { String eventName = GUID.generate(); - assertFalse(rmEventService.existsEvent(eventName)); - rmEventService.addEvent(EVENT_TYPE, eventName, DISPLAY_LABEL); - assertTrue(rmEventService.existsEvent(eventName)); + assertFalse(eventService.existsEvent(eventName)); + eventService.addEvent(EVENT_TYPE, eventName, DISPLAY_LABEL); + assertTrue(eventService.existsEvent(eventName)); sendRequest(new DeleteRequest(GET_EVENTS_URL + "/" + eventName),200); - assertFalse(rmEventService.existsEvent(eventName)); + assertFalse(eventService.existsEvent(eventName)); // Bad request sendRequest(new DeleteRequest(GET_EVENTS_URL + "/cheese"), 404); diff --git a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RMCaveatConfigScriptTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RMCaveatConfigScriptTest.java index 4caab0b1ca..c7e17a2ba6 100644 --- a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RMCaveatConfigScriptTest.java +++ b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RMCaveatConfigScriptTest.java @@ -23,8 +23,8 @@ import java.util.List; import org.alfresco.model.ContentModel; import org.alfresco.module.org_alfresco_module_rm.caveat.RMCaveatConfigService; +import org.alfresco.module.org_alfresco_module_rm.test.util.BaseRMWebScriptTestCase; import org.alfresco.repo.security.authentication.AuthenticationUtil; -import org.alfresco.repo.web.scripts.BaseWebScriptTest; import org.alfresco.service.cmr.security.MutableAuthenticationService; import org.alfresco.service.cmr.security.PersonService; import org.alfresco.util.PropertyMap; @@ -42,14 +42,12 @@ import org.springframework.extensions.webscripts.TestWebScriptServer.Response; * * @author Mark Rogers */ -public class RMCaveatConfigScriptTest extends BaseWebScriptTest +public class RMCaveatConfigScriptTest extends BaseRMWebScriptTestCase { private MutableAuthenticationService authenticationService; private RMCaveatConfigService caveatConfigService; private PersonService personService; - private static final String USER_ONE = "RMCaveatConfigTestOne"; - private static final String USER_TWO = "RMCaveatConfigTestTwo"; protected final static String RM_LIST = "rmc:smListTest"; protected final static String RM_LIST_URI_ELEM = "rmc_smListTest"; @@ -57,16 +55,13 @@ public class RMCaveatConfigScriptTest extends BaseWebScriptTest private static final String URL_RM_CONSTRAINTS = "/api/rma/admin/rmconstraints"; @Override - protected void setUp() throws Exception + protected void initServices() { - super.setUp(); - - this.caveatConfigService = (RMCaveatConfigService)getServer().getApplicationContext().getBean("CaveatConfigService"); + super.initServices(); + + this.caveatConfigService = (RMCaveatConfigService)getServer().getApplicationContext().getBean("CaveatConfigService"); this.authenticationService = (MutableAuthenticationService)getServer().getApplicationContext().getBean("AuthenticationService"); this.personService = (PersonService)getServer().getApplicationContext().getBean("PersonService"); - - // Set the current security context as admin - AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); } private void createUser(String userName) @@ -87,14 +82,6 @@ public class RMCaveatConfigScriptTest extends BaseWebScriptTest } } - @Override - protected void tearDown() throws Exception - { - super.tearDown(); - //this.authenticationComponent.setCurrentUser(AuthenticationUtil.getAdminUserName()); - - } - public void testGetRMConstraints() throws Exception { diff --git a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RMConstraintScriptTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RMConstraintScriptTest.java index b3cdd47bbc..6116258954 100644 --- a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RMConstraintScriptTest.java +++ b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RMConstraintScriptTest.java @@ -23,12 +23,11 @@ import java.util.List; import org.alfresco.model.ContentModel; import org.alfresco.module.org_alfresco_module_rm.caveat.RMCaveatConfigService; +import org.alfresco.module.org_alfresco_module_rm.test.util.BaseRMWebScriptTestCase; import org.alfresco.repo.security.authentication.AuthenticationUtil; -import org.alfresco.repo.web.scripts.BaseWebScriptTest; import org.alfresco.service.cmr.security.MutableAuthenticationService; import org.alfresco.service.cmr.security.PersonService; import org.alfresco.util.PropertyMap; -import org.json.JSONArray; import org.json.JSONObject; import org.springframework.extensions.webscripts.Status; import org.springframework.extensions.webscripts.TestWebScriptServer.GetRequest; @@ -39,7 +38,7 @@ import org.springframework.extensions.webscripts.TestWebScriptServer.Response; * * @author Mark Rogers */ -public class RMConstraintScriptTest extends BaseWebScriptTest +public class RMConstraintScriptTest extends BaseRMWebScriptTestCase { private MutableAuthenticationService authenticationService; private RMCaveatConfigService caveatConfigService; @@ -51,18 +50,13 @@ public class RMConstraintScriptTest extends BaseWebScriptTest private static final String URL_RM_CONSTRAINTS = "/api/rma/rmconstraints"; @Override - protected void setUp() throws Exception + protected void initServices() { + super.initServices(); + this.caveatConfigService = (RMCaveatConfigService)getServer().getApplicationContext().getBean("CaveatConfigService"); this.authenticationService = (MutableAuthenticationService)getServer().getApplicationContext().getBean("AuthenticationService"); this.personService = (PersonService)getServer().getApplicationContext().getBean("PersonService"); - super.setUp(); - } - - @Override - protected void tearDown() throws Exception - { - super.tearDown(); } /** @@ -108,32 +102,11 @@ public class RMConstraintScriptTest extends BaseWebScriptTest JSONObject data = top.getJSONObject("data"); System.out.println(response.getContentAsString()); - JSONArray allowedValues = data.getJSONArray("allowedValuesForCurrentUser"); - -// assertTrue("values not correct", compare(array, allowedValues)); - -// JSONArray constraintDetails = data.getJSONArray("constraintDetails"); -// -// assertTrue("details array does not contain 3 elements", constraintDetails.length() == 3); -// for(int i =0; i < constraintDetails.length(); i++) -// { -// JSONObject detail = constraintDetails.getJSONObject(i); -// } + data.getJSONArray("allowedValuesForCurrentUser"); + } - /** - * - * @throws Exception - */ - -// /** -// * Negative test - Attempt to get a constraint that does exist -// */ -// { -// String url = URL_RM_CONSTRAINTS + "/" + "rmc_wibble"; -// sendRequest(new GetRequest(url), Status.STATUS_NOT_FOUND); -// } -// + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); personService.deletePerson("fbloggs"); personService.deletePerson("jrogers"); diff --git a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RmRestApiTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RmRestApiTest.java index ed44c7db6e..24e0cce361 100644 --- a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RmRestApiTest.java +++ b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RmRestApiTest.java @@ -22,49 +22,19 @@ import java.io.IOException; import java.io.Serializable; import java.io.UnsupportedEncodingException; import java.text.MessageFormat; -import java.util.Calendar; import java.util.Date; -import java.util.HashMap; -import java.util.List; import java.util.Map; -import javax.transaction.UserTransaction; - -import org.alfresco.model.ContentModel; -import org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminService; import org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminServiceImpl; -import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; -import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService; -import org.alfresco.module.org_alfresco_module_rm.action.impl.CompleteEventAction; -import org.alfresco.module.org_alfresco_module_rm.audit.RecordsManagementAuditService; -import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionAction; -import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService; import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementCustomModel; import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; import org.alfresco.module.org_alfresco_module_rm.script.CustomReferenceType; +import org.alfresco.module.org_alfresco_module_rm.test.util.BaseRMWebScriptTestCase; import org.alfresco.module.org_alfresco_module_rm.test.util.TestActionParams; -import org.alfresco.module.org_alfresco_module_rm.test.util.TestUtilities; -import org.alfresco.repo.content.MimetypeMap; -import org.alfresco.repo.security.authentication.AuthenticationUtil; -import org.alfresco.repo.transaction.RetryingTransactionHelper; -import org.alfresco.repo.web.scripts.BaseWebScriptTest; -import org.alfresco.service.ServiceRegistry; import org.alfresco.service.cmr.dictionary.AspectDefinition; import org.alfresco.service.cmr.dictionary.AssociationDefinition; -import org.alfresco.service.cmr.dictionary.DictionaryService; -import org.alfresco.service.cmr.repository.ChildAssociationRef; -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; -import org.alfresco.service.cmr.repository.StoreRef; -import org.alfresco.service.cmr.search.ResultSet; -import org.alfresco.service.cmr.search.SearchService; -import org.alfresco.service.cmr.view.ImporterService; -import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; -import org.alfresco.service.namespace.RegexQNamePattern; -import org.alfresco.service.transaction.TransactionService; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -82,7 +52,7 @@ import org.springframework.extensions.webscripts.TestWebScriptServer.Response; * * @author Neil McErlean */ -public class RmRestApiTest extends BaseWebScriptTest implements RecordsManagementModel +public class RmRestApiTest extends BaseRMWebScriptTestCase implements RecordsManagementModel { protected static final String GET_NODE_AUDITLOG_URL_FORMAT = "/api/node/{0}/rmauditlog"; protected static final String GET_TRANSFER_URL_FORMAT = "/api/node/{0}/transfers/{1}"; @@ -95,60 +65,60 @@ public class RmRestApiTest extends BaseWebScriptTest implements RecordsManagemen protected static final String APPLICATION_JSON = "application/json"; protected static final String RMA_CUSTOM_PROPS_DEFINITIONS_URL = "/api/rma/admin/custompropertydefinitions"; protected static final String RMA_CUSTOM_REFS_DEFINITIONS_URL = "/api/rma/admin/customreferencedefinitions"; - protected NamespaceService namespaceService; - protected NodeService nodeService; - protected ContentService contentService; - protected DictionaryService dictionaryService; - protected SearchService searchService; - protected ImporterService importService; - protected TransactionService transactionService; - protected ServiceRegistry services; - protected RecordsManagementService rmService; - protected RecordsManagementActionService rmActionService; - protected RecordsManagementAuditService rmAuditService; - protected RecordsManagementAdminService rmAdminService; - protected RetryingTransactionHelper transactionHelper; - protected DispositionService dispositionService; +// protected NamespaceService namespaceService; +// protected NodeService nodeService; +// protected ContentService contentService; +// protected DictionaryService dictionaryService; +// protected SearchService searchService; +// protected ImporterService importService; +// protected TransactionService transactionService; +// protected ServiceRegistry services; +// protected RecordsManagementService rmService; +// protected RecordsManagementActionService rmActionService; +// protected RecordsManagementAuditService rmAuditService; +// protected RecordsManagementAdminService rmAdminService; +// protected RetryingTransactionHelper transactionHelper; +// protected DispositionService dispositionService; private static final String BI_DI = "BiDi"; private static final String CHILD_SRC = "childSrc"; private static final String CHILD_TGT = "childTgt"; - @Override - protected void setUp() throws Exception - { - setCustomContext("classpath:test-context.xml"); - - super.setUp(); - this.namespaceService = (NamespaceService) getServer().getApplicationContext().getBean("NamespaceService"); - this.nodeService = (NodeService) getServer().getApplicationContext().getBean("NodeService"); - this.contentService = (ContentService)getServer().getApplicationContext().getBean("ContentService"); - this.dictionaryService = (DictionaryService)getServer().getApplicationContext().getBean("DictionaryService"); - this.searchService = (SearchService)getServer().getApplicationContext().getBean("SearchService"); - this.importService = (ImporterService)getServer().getApplicationContext().getBean("ImporterService"); - this.transactionService = (TransactionService)getServer().getApplicationContext().getBean("TransactionService"); - this.services = (ServiceRegistry)getServer().getApplicationContext().getBean("ServiceRegistry"); - this.rmService = (RecordsManagementService)getServer().getApplicationContext().getBean("RecordsManagementService"); - this.rmActionService = (RecordsManagementActionService)getServer().getApplicationContext().getBean("RecordsManagementActionService"); - this.rmAuditService = (RecordsManagementAuditService)getServer().getApplicationContext().getBean("RecordsManagementAuditService"); - this.rmAdminService = (RecordsManagementAdminService)getServer().getApplicationContext().getBean("RecordsManagementAdminService"); - transactionHelper = (RetryingTransactionHelper)getServer().getApplicationContext().getBean("retryingTransactionHelper"); - dispositionService = (DispositionService)getServer().getApplicationContext().getBean("DispositionService"); - - AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); - - // Bring the filePlan into the test database. - // - // This is quite a slow call, so if this class grew to have many test methods, - // there would be a real benefit in using something like @BeforeClass for the line below. - transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() - { - public NodeRef execute() throws Throwable - { - return TestUtilities.loadFilePlanData(getServer().getApplicationContext()); - } - }); - } +// @Override +// protected void setUp() throws Exception +// { +// setCustomContext("classpath:test-context.xml"); +// +// super.setUp(); +// this.namespaceService = (NamespaceService) getServer().getApplicationContext().getBean("NamespaceService"); +// this.nodeService = (NodeService) getServer().getApplicationContext().getBean("NodeService"); +// this.contentService = (ContentService)getServer().getApplicationContext().getBean("ContentService"); +// this.dictionaryService = (DictionaryService)getServer().getApplicationContext().getBean("DictionaryService"); +// this.searchService = (SearchService)getServer().getApplicationContext().getBean("SearchService"); +// this.importService = (ImporterService)getServer().getApplicationContext().getBean("ImporterService"); +// this.transactionService = (TransactionService)getServer().getApplicationContext().getBean("TransactionService"); +// this.services = (ServiceRegistry)getServer().getApplicationContext().getBean("ServiceRegistry"); +// this.rmService = (RecordsManagementService)getServer().getApplicationContext().getBean("RecordsManagementService"); +// this.rmActionService = (RecordsManagementActionService)getServer().getApplicationContext().getBean("RecordsManagementActionService"); +// this.rmAuditService = (RecordsManagementAuditService)getServer().getApplicationContext().getBean("RecordsManagementAuditService"); +// this.rmAdminService = (RecordsManagementAdminService)getServer().getApplicationContext().getBean("RecordsManagementAdminService"); +// transactionHelper = (RetryingTransactionHelper)getServer().getApplicationContext().getBean("retryingTransactionHelper"); +// dispositionService = (DispositionService)getServer().getApplicationContext().getBean("DispositionService"); +// +// AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); +// +// // Bring the filePlan into the test database. +// // +// // This is quite a slow call, so if this class grew to have many test methods, +// // there would be a real benefit in using something like @BeforeClass for the line below. +// transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() +// { +// public NodeRef execute() throws Throwable +// { +// return TestUtilities.loadFilePlanData(getServer().getApplicationContext()); +// } +// }); +// } /** * This test method ensures that a POST of an RM action to a non-existent node @@ -156,12 +126,8 @@ public class RmRestApiTest extends BaseWebScriptTest implements RecordsManagemen * * @throws Exception */ - // TODO taken out for now - public void xtestPostActionToNonExistentNode() throws Exception + public void testPostActionToNonExistentNode() throws Exception { - NodeRef recordCategory = TestUtilities.getRecordCategory(searchService, "Reports", "AIS Audit Records"); - assertNotNull(recordCategory); - NodeRef nonExistentNode = new NodeRef("workspace://SpacesStore/09ca1e02-1c87-4a53-97e7-xxxxxxxxxxxx"); // Construct the JSON request. @@ -181,24 +147,7 @@ public class RmRestApiTest extends BaseWebScriptTest implements RecordsManagemen public void testPostReviewedAction() throws IOException, JSONException { - // Get the recordCategory under which we will create the testNode. - NodeRef recordCategory = TestUtilities.getRecordCategory(searchService, "Reports", "AIS Audit Records"); - assertNotNull(recordCategory); - - NodeRef recordFolder = TestUtilities.getRecordFolder(searchService, "Reports", "AIS Audit Records", "January AIS Audit Records"); - assertNotNull(recordFolder); - - // Create a testNode/file which is to be declared as a record. - NodeRef testRecord = this.nodeService.createNode(recordFolder, - ContentModel.ASSOC_CONTAINS, - QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "MyRecord.txt"), - ContentModel.TYPE_CONTENT).getChildRef(); - - // Set some dummy content. - ContentWriter writer = this.contentService.getWriter(testRecord, ContentModel.PROP_CONTENT, true); - writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); - writer.setEncoding("UTF-8"); - writer.putContent("There is some content in this record"); + NodeRef testRecord = utils.createRecord(recordFolder, "test.txt"); // In this test, this property has a date-value equal to the model import time. Serializable pristineReviewAsOf = this.nodeService.getProperty(testRecord, PROP_REVIEW_AS_OF); @@ -230,46 +179,9 @@ public class RmRestApiTest extends BaseWebScriptTest implements RecordsManagemen public void testPostMultiReviewedAction() throws IOException, JSONException { - // Get the recordCategory under which we will create the testNode. - NodeRef recordCategory = TestUtilities.getRecordCategory(searchService, "Reports", "AIS Audit Records"); - assertNotNull(recordCategory); - - NodeRef recordFolder = TestUtilities.getRecordFolder(searchService, "Reports", "AIS Audit Records", "January AIS Audit Records"); - assertNotNull(recordFolder); - - // Create a testNode/file which is to be declared as a record. - NodeRef testRecord = this.nodeService.createNode(recordFolder, - ContentModel.ASSOC_CONTAINS, - QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "MyRecord.txt"), - ContentModel.TYPE_CONTENT).getChildRef(); - - // Set some dummy content. - ContentWriter writer = this.contentService.getWriter(testRecord, ContentModel.PROP_CONTENT, true); - writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); - writer.setEncoding("UTF-8"); - writer.putContent("There is some content in this record"); - - NodeRef testRecord2 = this.nodeService.createNode(recordFolder, - ContentModel.ASSOC_CONTAINS, - QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "MyRecord2.txt"), - ContentModel.TYPE_CONTENT).getChildRef(); - - // Set some dummy content. - writer = this.contentService.getWriter(testRecord2, ContentModel.PROP_CONTENT, true); - writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); - writer.setEncoding("UTF-8"); - writer.putContent("There is some content in this record"); - - NodeRef testRecord3 = this.nodeService.createNode(recordFolder, - ContentModel.ASSOC_CONTAINS, - QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "MyRecord3.txt"), - ContentModel.TYPE_CONTENT).getChildRef(); - - // Set some dummy content. - writer = this.contentService.getWriter(testRecord3, ContentModel.PROP_CONTENT, true); - writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); - writer.setEncoding("UTF-8"); - writer.putContent("There is some content in this record"); + NodeRef testRecord = utils.createRecord(recordFolder, "test1.txt"); + NodeRef testRecord2 = utils.createRecord(recordFolder, "test2.txt"); + NodeRef testRecord3 = utils.createRecord(recordFolder, "test3.txt"); // In this test, this property has a date-value equal to the model import time. Serializable pristineReviewAsOf = this.nodeService.getProperty(testRecord, PROP_REVIEW_AS_OF); @@ -332,8 +244,8 @@ public class RmRestApiTest extends BaseWebScriptTest implements RecordsManagemen // Submit the JSON request. final int expectedStatus = 200; //TODO Currently failing unit test. -// Response rsp = sendRequest(new PostRequest(RMA_ACTIONS_URL, -// jsonString, APPLICATION_JSON), expectedStatus); + sendRequest(new PostRequest(RMA_ACTIONS_URL, + jsonString, APPLICATION_JSON), expectedStatus); } public void testPostCustomReferenceDefinitions() throws IOException, JSONException @@ -402,11 +314,11 @@ public class RmRestApiTest extends BaseWebScriptTest implements RecordsManagemen dictionaryService.getAspect(QName.createQName(RecordsManagementAdminServiceImpl.RMC_CUSTOM_ASSOCS, namespaceService)); assertNotNull("Missing customAssocs aspect", customAssocsAspect); - QName newRefQname = rmAdminService.getQNameForClientId(generatedChildRefId); + QName newRefQname = adminService.getQNameForClientId(generatedChildRefId); Map associations = customAssocsAspect.getAssociations(); assertTrue("Custom child assoc not returned by dataDictionary.", associations.containsKey(newRefQname)); - newRefQname = rmAdminService.getQNameForClientId(generatedBidiRefId); + newRefQname = adminService.getQNameForClientId(generatedBidiRefId); assertTrue("Custom std assoc not returned by dataDictionary.", customAssocsAspect.getAssociations().containsKey(newRefQname)); return result; @@ -609,10 +521,8 @@ public class RmRestApiTest extends BaseWebScriptTest implements RecordsManagemen public void testGetPostAndRemoveCustomReferenceInstances() throws Exception { - // Create test records. - NodeRef recordFolder = retrievePreexistingRecordFolder(); - NodeRef testRecord1 = createRecord(recordFolder, "testRecord1" + System.currentTimeMillis(), "The from recørd"); - NodeRef testRecord2 = createRecord(recordFolder, "testRecord2" + System.currentTimeMillis(), "The to récord"); + NodeRef testRecord1 = utils.createRecord(recordFolder, "testRecord1" + System.currentTimeMillis(), "The from recørd"); + NodeRef testRecord2 = utils.createRecord(recordFolder, "testRecord2" + System.currentTimeMillis(), "The to récord"); String node1Url = testRecord1.toString().replace("://", "/"); String refInstancesRecord1Url = MessageFormat.format(REF_INSTANCES_URL_FORMAT, node1Url); @@ -722,9 +632,8 @@ public class RmRestApiTest extends BaseWebScriptTest implements RecordsManagemen public void testMob1630ShouldNotBeAbleToCreateTwoSupersedesReferencesOnOneRecordPair() throws Exception { // Create 2 test records. - NodeRef recordFolder = retrievePreexistingRecordFolder(); - NodeRef testRecord1 = createRecord(recordFolder, "testRecord1" + System.currentTimeMillis(), "The from recørd"); - NodeRef testRecord2 = createRecord(recordFolder, "testRecord2" + System.currentTimeMillis(), "The to récord"); + NodeRef testRecord1 = utils.createRecord(recordFolder, "testRecord1" + System.currentTimeMillis(), "The from recørd"); + NodeRef testRecord2 = utils.createRecord(recordFolder, "testRecord2" + System.currentTimeMillis(), "The to récord"); String node1Url = testRecord1.toString().replace("://", "/"); String node2Url = testRecord2.toString().replace("://", "/"); @@ -1042,27 +951,19 @@ public class RmRestApiTest extends BaseWebScriptTest implements RecordsManagemen JSONArray aspects = dataObj.getJSONArray("recordMetaDataAspects"); assertNotNull(aspects); - assertEquals(5, aspects.length()); + assertEquals(4, aspects.length()); // TODO test the items themselves } public void testExport() throws Exception { - NodeRef recordFolder1 = TestUtilities.getRecordFolder(searchService, "Reports", - "AIS Audit Records", "January AIS Audit Records"); - assertNotNull(recordFolder1); - - NodeRef recordFolder2 = TestUtilities.getRecordFolder(searchService, "Reports", - "Unit Manning Documents", "1st Quarter Unit Manning Documents"); - assertNotNull(recordFolder2); - String exportUrl = "/api/rma/admin/export"; // define JSON POST body JSONObject jsonPostData = new JSONObject(); JSONArray nodeRefs = new JSONArray(); - nodeRefs.put(recordFolder1.toString()); + nodeRefs.put(recordFolder.toString()); nodeRefs.put(recordFolder2.toString()); jsonPostData.put("nodeRefs", nodeRefs); String jsonPostString = jsonPostData.toString(); @@ -1074,20 +975,12 @@ public class RmRestApiTest extends BaseWebScriptTest implements RecordsManagemen public void testExportInTransferFormat() throws Exception { - NodeRef recordFolder1 = TestUtilities.getRecordFolder(searchService, "Reports", - "AIS Audit Records", "January AIS Audit Records"); - assertNotNull(recordFolder1); - - NodeRef recordFolder2 = TestUtilities.getRecordFolder(searchService, "Reports", - "Unit Manning Documents", "1st Quarter Unit Manning Documents"); - assertNotNull(recordFolder2); - String exportUrl = "/api/rma/admin/export"; // define JSON POST body JSONObject jsonPostData = new JSONObject(); JSONArray nodeRefs = new JSONArray(); - nodeRefs.put(recordFolder1.toString()); + nodeRefs.put(recordFolder.toString()); nodeRefs.put(recordFolder2.toString()); jsonPostData.put("nodeRefs", nodeRefs); jsonPostData.put("transferFormat", true); @@ -1098,216 +991,6 @@ public class RmRestApiTest extends BaseWebScriptTest implements RecordsManagemen assertEquals("application/zip", rsp.getContentType()); } - public void testTransfer() throws Exception - { - // Test 404 status for non existent node - String transferId = "yyy"; - String nonExistentNode = "workspace/SpacesStore/09ca1e02-1c87-4a53-97e7-xxxxxxxxxxxx"; - String nonExistentUrl = MessageFormat.format(GET_TRANSFER_URL_FORMAT, nonExistentNode, transferId); - Response rsp = sendRequest(new GetRequest(nonExistentUrl), 404); - - // Test 400 status for node that isn't a file plan - NodeRef series = TestUtilities.getRecordSeries(searchService, "Reports"); - assertNotNull(series); - String seriesNodeUrl = series.toString().replace("://", "/"); - String wrongNodeUrl = MessageFormat.format(GET_TRANSFER_URL_FORMAT, seriesNodeUrl, transferId); - rsp = sendRequest(new GetRequest(wrongNodeUrl), 400); - - // Test 404 status for file plan with no transfers - NodeRef rootNode = this.rmService.getFilePlan(series); - String rootNodeUrl = rootNode.toString().replace("://", "/"); - String transferUrl = MessageFormat.format(GET_TRANSFER_URL_FORMAT, rootNodeUrl, transferId); - rsp = sendRequest(new GetRequest(transferUrl), 404); - - // Get test in state where a transfer will be present - NodeRef recordCategory = TestUtilities.getRecordCategory(searchService, "Civilian Files", "Foreign Employee Award Files"); - assertNotNull(recordCategory); - - UserTransaction txn = transactionService.getUserTransaction(false); - txn.begin(); - - NodeRef newRecordFolder = this.nodeService.createNode(recordCategory, ContentModel.ASSOC_CONTAINS, - QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, QName.createValidLocalName("recordFolder")), - TYPE_RECORD_FOLDER).getChildRef(); - Map folderProps = new HashMap(1); - folderProps.put(PROP_IDENTIFIER, "2009-000000" + nodeService.getProperty(newRecordFolder, ContentModel.PROP_NODE_DBID)); - nodeService.addProperties(newRecordFolder, folderProps); - - txn.commit(); - txn = transactionService.getUserTransaction(false); - txn.begin(); - - // Create 2 documents - Map props1 = new HashMap(1); - props1.put(ContentModel.PROP_NAME, "record1"); - NodeRef recordOne = this.nodeService.createNode(newRecordFolder, - ContentModel.ASSOC_CONTAINS, - QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "record1"), - ContentModel.TYPE_CONTENT, props1).getChildRef(); - - // Set the content - ContentWriter writer1 = this.contentService.getWriter(recordOne, ContentModel.PROP_CONTENT, true); - writer1.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); - writer1.setEncoding("UTF-8"); - writer1.putContent("There is some content for record 1"); - - Map props2 = new HashMap(1); - props2.put(ContentModel.PROP_NAME, "record2"); - NodeRef recordTwo = this.nodeService.createNode(newRecordFolder, - ContentModel.ASSOC_CONTAINS, - QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "record2"), - ContentModel.TYPE_CONTENT, props2).getChildRef(); - - // Set the content - ContentWriter writer2 = this.contentService.getWriter(recordTwo, ContentModel.PROP_CONTENT, true); - writer2.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); - writer2.setEncoding("UTF-8"); - writer2.putContent("There is some content for record 2"); - txn.commit(); - - // declare the new record - txn = transactionService.getUserTransaction(false); - txn.begin(); - declareRecord(recordOne); - declareRecord(recordTwo); - - // prepare for the transfer - Map params = new HashMap(3); - params.put(CompleteEventAction.PARAM_EVENT_NAME, "case_complete"); - params.put(CompleteEventAction.PARAM_EVENT_COMPLETED_AT, new Date()); - params.put(CompleteEventAction.PARAM_EVENT_COMPLETED_BY, "gavinc"); - this.rmActionService.executeRecordsManagementAction(newRecordFolder, "completeEvent", params); - this.rmActionService.executeRecordsManagementAction(newRecordFolder, "cutoff"); - - DispositionAction da = dispositionService.getNextDispositionAction(newRecordFolder); - assertNotNull(da); - assertEquals("transfer", da.getName()); - txn.commit(); - - // Clock the asOf date back to ensure eligibility - txn = transactionService.getUserTransaction(false); - txn.begin(); - Calendar calendar = Calendar.getInstance(); - calendar.set(Calendar.HOUR, 0); - calendar.set(Calendar.MINUTE, 0); - calendar.set(Calendar.SECOND, 0); - this.nodeService.setProperty(da.getNodeRef(), PROP_DISPOSITION_AS_OF, calendar.getTime()); - - // Do the transfer - this.rmActionService.executeRecordsManagementAction(newRecordFolder, "transfer", null); - txn.commit(); - - // check that there is a transfer object present - List assocs = this.nodeService.getChildAssocs(rootNode, ASSOC_TRANSFERS, RegexQNamePattern.MATCH_ALL); - assertNotNull(assocs); - assertTrue(assocs.size() > 0); - - // Test 404 status for file plan with transfers but not the requested one - rootNode = this.rmService.getFilePlan(newRecordFolder); - rootNodeUrl = rootNode.toString().replace("://", "/"); - transferUrl = MessageFormat.format(GET_TRANSFER_URL_FORMAT, rootNodeUrl, transferId); - rsp = sendRequest(new GetRequest(transferUrl), 404); - - // retrieve the id of the last transfer - NodeRef transferNodeRef = assocs.get(assocs.size()-1).getChildRef(); - Date transferDate = (Date)nodeService.getProperty(transferNodeRef, ContentModel.PROP_CREATED); - - // Test successful retrieval of transfer archive - transferId = transferNodeRef.getId(); - transferUrl = MessageFormat.format(GET_TRANSFER_URL_FORMAT, rootNodeUrl, transferId); - rsp = sendRequest(new GetRequest(transferUrl), 200); - assertEquals("application/zip", rsp.getContentType()); - - // Test retrieval of transfer report, will be in JSON format - String transferReportUrl = MessageFormat.format(TRANSFER_REPORT_URL_FORMAT, rootNodeUrl, transferId); - rsp = sendRequest(new GetRequest(transferReportUrl), 200); - //System.out.println(rsp.getContentAsString()); - assertEquals("application/json", rsp.getContentType()); - JSONObject jsonRsp = new JSONObject(new JSONTokener(rsp.getContentAsString())); - assertTrue(jsonRsp.has("data")); - JSONObject data = jsonRsp.getJSONObject("data"); - assertTrue(data.has("transferDate")); - Date transferDateRsp = ISO8601DateFormat.parse(data.getString("transferDate")); - assertEquals(transferDate, transferDateRsp); - assertTrue(data.has("transferPerformedBy")); - assertEquals("System", data.getString("transferPerformedBy")); - assertTrue(data.has("dispositionAuthority")); - assertEquals("N1-218-00-3 item 18", data.getString("dispositionAuthority")); - assertTrue(data.has("items")); - JSONArray items = data.getJSONArray("items"); - assertEquals("Expecting 1 transferred folder", 1, items.length()); - JSONObject folder = items.getJSONObject(0); - assertTrue(folder.has("type")); - assertEquals("folder", folder.getString("type")); - assertTrue(folder.has("name")); - assertTrue(folder.getString("name").length() > 0); - assertTrue(folder.has("nodeRef")); - assertTrue(folder.getString("nodeRef").startsWith("workspace://SpacesStore/")); - assertTrue(folder.has("id")); - - // "id" should start with year-number pattern e.g. 2009-0000 - // This regular expression represents a string that starts with 4 digits followed by a hyphen, - // then 4 more digits and then any other characters - final String idRegExp = "^\\d{4}-\\d{4}.*"; - assertTrue(folder.getString("id").matches(idRegExp)); - - assertTrue(folder.has("children")); - JSONArray records = folder.getJSONArray("children"); - assertEquals("Expecting 2 transferred records", 2, records.length()); - JSONObject record1 = records.getJSONObject(0); - assertTrue(record1.has("type")); - assertEquals("record", record1.getString("type")); - assertTrue(record1.has("name")); - assertEquals("record1", record1.getString("name")); - assertTrue(record1.has("nodeRef")); - assertTrue(record1.getString("nodeRef").startsWith("workspace://SpacesStore/")); - assertTrue(record1.has("id")); - assertTrue(record1.getString("id").matches(idRegExp)); - assertTrue(record1.has("declaredBy")); - assertEquals("System", record1.getString("declaredBy")); - assertTrue(record1.has("declaredAt")); - - // Test filing a transfer report as a record - - // Attempt to store transfer report at non existent destination, make sure we get 404 - JSONObject jsonPostData = new JSONObject(); - jsonPostData.put("destination", "workspace://SpacesStore/09ca1e02-1c87-4a53-97e7-xxxxxxxxxxxx"); - String jsonPostString = jsonPostData.toString(); - rsp = sendRequest(new PostRequest(transferReportUrl, jsonPostString, APPLICATION_JSON), 404); - - // Attempt to store audit log at wrong type of destination, make sure we get 400 - NodeRef wrongCategory = TestUtilities.getRecordCategory(searchService, "Civilian Files", - "Foreign Employee Award Files"); - assertNotNull(wrongCategory); - jsonPostData = new JSONObject(); - jsonPostData.put("destination", wrongCategory.toString()); - jsonPostString = jsonPostData.toString(); - rsp = sendRequest(new PostRequest(transferReportUrl, jsonPostString, APPLICATION_JSON), 400); - - // get record folder to file into - NodeRef destination = TestUtilities.getRecordFolder(searchService, "Civilian Files", - "Foreign Employee Award Files", "Christian Bohr"); - assertNotNull(destination); - - // Store the full audit log as a record - jsonPostData = new JSONObject(); - jsonPostData.put("destination", destination); - jsonPostString = jsonPostData.toString(); - rsp = sendRequest(new PostRequest(transferReportUrl, jsonPostString, APPLICATION_JSON), 200); - - // check the response - System.out.println(rsp.getContentAsString()); - jsonRsp = new JSONObject(new JSONTokener(rsp.getContentAsString())); - assertTrue(jsonRsp.has("success")); - assertTrue(jsonRsp.getBoolean("success")); - assertTrue(jsonRsp.has("record")); - assertNotNull(jsonRsp.get("record")); - assertTrue(nodeService.exists(new NodeRef(jsonRsp.getString("record")))); - assertTrue(jsonRsp.has("recordName")); - assertNotNull(jsonRsp.get("recordName")); - assertTrue(jsonRsp.getString("recordName").startsWith("report_")); - } - public void testAudit() throws Exception { // call the list service to get audit events @@ -1321,7 +1004,7 @@ public class RmRestApiTest extends BaseWebScriptTest implements RecordsManagemen JSONObject data = jsonParsedObject.getJSONObject("data"); JSONObject events = data.getJSONObject("auditEvents"); JSONArray items = events.getJSONArray("items"); - assertEquals(this.rmAuditService.getAuditEvents().size(), items.length()); + assertEquals(auditService.getAuditEvents().size(), items.length()); assertTrue(items.length() > 0); JSONObject item = items.getJSONObject(0); assertTrue(item.length() == 2); @@ -1342,10 +1025,6 @@ public class RmRestApiTest extends BaseWebScriptTest implements RecordsManagemen assertEquals("application/json", rsp.getContentType()); jsonRsp = new JSONObject(new JSONTokener(rsp.getContentAsString())); - // get category - NodeRef recordCategory = TestUtilities.getRecordCategory(searchService, "Civilian Files", "Foreign Employee Award Files"); - assertNotNull(recordCategory); - // construct the URL String nodeUrl = recordCategory.toString().replace("://", "/"); String auditUrl = MessageFormat.format(GET_NODE_AUDITLOG_URL_FORMAT, nodeUrl); @@ -1369,7 +1048,7 @@ public class RmRestApiTest extends BaseWebScriptTest implements RecordsManagemen assertEquals("application/json", rsp.getContentType()); jsonRsp = new JSONObject(new JSONTokener(rsp.getContentAsString())); - checkAuditStatus(true); + checkAuditStatus(false); // start the RM audit log JSONObject jsonPostData = new JSONObject(); @@ -1431,22 +1110,15 @@ public class RmRestApiTest extends BaseWebScriptTest implements RecordsManagemen Response rsp = sendRequest(new PostRequest(RMA_AUDITLOG_URL, jsonPostString, APPLICATION_JSON), 404); // Attempt to store audit log at wrong type of destination, make sure we get 400 - NodeRef recordCategory = TestUtilities.getRecordCategory(searchService, "Civilian Files", - "Foreign Employee Award Files"); - assertNotNull(recordCategory); jsonPostData = new JSONObject(); jsonPostData.put("destination", recordCategory.toString()); jsonPostString = jsonPostData.toString(); rsp = sendRequest(new PostRequest(RMA_AUDITLOG_URL, jsonPostString, APPLICATION_JSON), 400); - // get record folder to file into - NodeRef destination = TestUtilities.getRecordFolder(searchService, "Civilian Files", - "Foreign Employee Award Files", "Christian Bohr"); - assertNotNull(destination); // Store the full audit log as a record jsonPostData = new JSONObject(); - jsonPostData.put("destination", destination); + jsonPostData.put("destination", recordFolder2); jsonPostString = jsonPostData.toString(); rsp = sendRequest(new PostRequest(RMA_AUDITLOG_URL, jsonPostString, APPLICATION_JSON), 200); @@ -1464,7 +1136,7 @@ public class RmRestApiTest extends BaseWebScriptTest implements RecordsManagemen // Store a filtered audit log as a record jsonPostData = new JSONObject(); - jsonPostData.put("destination", destination); + jsonPostData.put("destination", recordFolder2); jsonPostData.put("size", "50"); jsonPostData.put("user", "gavinc"); jsonPostData.put("event", "Update Metadata"); @@ -1484,63 +1156,6 @@ public class RmRestApiTest extends BaseWebScriptTest implements RecordsManagemen assertNotNull(jsonRsp.get("recordName")); assertTrue(jsonRsp.getString("recordName").startsWith("audit_")); } - - private void declareRecord(NodeRef recordOne) - { - // Declare record - Map propValues = this.nodeService.getProperties(recordOne); - propValues.put(RecordsManagementModel.PROP_PUBLICATION_DATE, new Date()); - /*List smList = new ArrayList(2); - smList.add("FOUO"); - smList.add("NOFORN"); - propValues.put(RecordsManagementModel.PROP_SUPPLEMENTAL_MARKING_LIST, (Serializable)smList);*/ - propValues.put(RecordsManagementModel.PROP_MEDIA_TYPE, "mediaTypeValue"); - propValues.put(RecordsManagementModel.PROP_FORMAT, "formatValue"); - propValues.put(RecordsManagementModel.PROP_DATE_RECEIVED, new Date()); - propValues.put(RecordsManagementModel.PROP_ORIGINATOR, "origValue"); - propValues.put(RecordsManagementModel.PROP_ORIGINATING_ORGANIZATION, "origOrgValue"); - propValues.put(ContentModel.PROP_TITLE, "titleValue"); - this.nodeService.setProperties(recordOne, propValues); - this.rmActionService.executeRecordsManagementAction(recordOne, "declareRecord"); - } - - private NodeRef retrievePreexistingRecordFolder() - { - final List resultNodeRefs = retrieveJanuaryAISVitalFolders(); - - return resultNodeRefs.get(0); - } - - private List retrieveJanuaryAISVitalFolders() - { - String typeQuery = "TYPE:\"" + TYPE_RECORD_FOLDER + "\" AND @cm\\:name:\"January AIS Audit Records\""; - ResultSet types = this.searchService.query(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, SearchService.LANGUAGE_LUCENE, typeQuery); - - final List resultNodeRefs = types.getNodeRefs(); - types.close(); - return resultNodeRefs; - } - - private NodeRef createRecord(NodeRef recordFolder, String name, String title) - { - // Create the document - Map props = new HashMap(1); - props.put(ContentModel.PROP_NAME, name); - props.put(ContentModel.PROP_TITLE, title); - NodeRef recordOne = this.nodeService.createNode(recordFolder, - ContentModel.ASSOC_CONTAINS, - QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, name), - ContentModel.TYPE_CONTENT, - props).getChildRef(); - - // Set the content - ContentWriter writer = this.contentService.getWriter(recordOne, ContentModel.PROP_CONTENT, true); - writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); - writer.setEncoding("UTF-8"); - writer.putContent("There is some content in this record"); - - return recordOne; - } public void testPropertyLabelWithAccentedChars() throws Exception { diff --git a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RoleRestApiTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RoleRestApiTest.java index a7d870fbce..6130973037 100644 --- a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RoleRestApiTest.java +++ b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/webscript/RoleRestApiTest.java @@ -19,19 +19,11 @@ package org.alfresco.module.org_alfresco_module_rm.test.webscript; import java.util.HashSet; -import java.util.List; import java.util.Set; -import org.alfresco.model.ContentModel; -import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; import org.alfresco.module.org_alfresco_module_rm.capability.Capability; import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; -import org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService; -import org.alfresco.repo.security.authentication.AuthenticationUtil; -import org.alfresco.repo.web.scripts.BaseWebScriptTest; -import org.alfresco.service.cmr.repository.NodeRef; -import org.alfresco.service.cmr.repository.NodeService; -import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.module.org_alfresco_module_rm.test.util.BaseRMWebScriptTestCase; import org.alfresco.util.GUID; import org.json.JSONArray; import org.json.JSONObject; @@ -46,41 +38,11 @@ import org.springframework.extensions.webscripts.TestWebScriptServer.Response; * * @author Roy Wetherall */ -public class RoleRestApiTest extends BaseWebScriptTest implements RecordsManagementModel +public class RoleRestApiTest extends BaseRMWebScriptTestCase implements RecordsManagementModel { - protected static StoreRef SPACES_STORE = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "SpacesStore"); protected static final String GET_ROLES_URL = "/api/rma/admin/rmroles"; protected static final String SERVICE_URL_PREFIX = "/alfresco/service"; - protected static final String APPLICATION_JSON = "application/json"; - - protected NodeService nodeService; - protected RecordsManagementService rmService; - protected RecordsManagementSecurityService rmSecurityService; - - private NodeRef rmRootNode; - - @Override - protected void setUp() throws Exception - { - super.setUp(); - this.nodeService = (NodeService) getServer().getApplicationContext().getBean("NodeService"); - this.rmService = (RecordsManagementService)getServer().getApplicationContext().getBean("RecordsManagementService"); - this.rmSecurityService = (RecordsManagementSecurityService)getServer().getApplicationContext().getBean("RecordsManagementSecurityService"); - - AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); - - List roots = rmService.getFilePlans(); - if (roots.size() != 0) - { - rmRootNode = roots.get(0); - } - else - { - NodeRef root = this.nodeService.getRootNode(new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "SpacesStore")); - rmRootNode = this.nodeService.createNode(root, ContentModel.ASSOC_CHILDREN, ContentModel.ASSOC_CHILDREN, TYPE_FILE_PLAN).getChildRef(); - } - - } + protected static final String APPLICATION_JSON = "application/json"; public void testGetRoles() throws Exception { @@ -88,11 +50,11 @@ public class RoleRestApiTest extends BaseWebScriptTest implements RecordsManagem String role2 = GUID.generate(); // Create a couple or roles by hand - rmSecurityService.createRole(rmRootNode, role1, "My Test Role", getListOfCapabilities(5)); - rmSecurityService.createRole(rmRootNode, role2, "My Test Role Too", getListOfCapabilities(5)); + securityService.createRole(filePlan, role1, "My Test Role", getListOfCapabilities(5)); + securityService.createRole(filePlan, role2, "My Test Role Too", getListOfCapabilities(5)); // Add the admin user to one of the roles - rmSecurityService.assignRoleToAuthority(rmRootNode, role1, "admin"); + securityService.assignRoleToAuthority(filePlan, role1, "admin"); try { @@ -141,8 +103,8 @@ public class RoleRestApiTest extends BaseWebScriptTest implements RecordsManagem finally { // Clean up - rmSecurityService.deleteRole(rmRootNode, role1); - rmSecurityService.deleteRole(rmRootNode, role2); + securityService.deleteRole(filePlan, role1); + securityService.deleteRole(filePlan, role2); } } @@ -181,7 +143,7 @@ public class RoleRestApiTest extends BaseWebScriptTest implements RecordsManagem } finally { - rmSecurityService.deleteRole(rmRootNode, roleName); + securityService.deleteRole(filePlan, roleName); } } @@ -189,7 +151,7 @@ public class RoleRestApiTest extends BaseWebScriptTest implements RecordsManagem public void testPutRole() throws Exception { String role1 = GUID.generate(); - rmSecurityService.createRole(rmRootNode, role1, "My Test Role", getListOfCapabilities(5)); + securityService.createRole(filePlan, role1, "My Test Role", getListOfCapabilities(5)); try { @@ -227,7 +189,7 @@ public class RoleRestApiTest extends BaseWebScriptTest implements RecordsManagem finally { // Clean up - rmSecurityService.deleteRole(rmRootNode, role1); + securityService.deleteRole(filePlan, role1); } } @@ -235,7 +197,7 @@ public class RoleRestApiTest extends BaseWebScriptTest implements RecordsManagem public void testGetRole() throws Exception { String role1 = GUID.generate(); - rmSecurityService.createRole(rmRootNode, role1, "My Test Role", getListOfCapabilities(5)); + securityService.createRole(filePlan, role1, "My Test Role", getListOfCapabilities(5)); try { @@ -260,7 +222,7 @@ public class RoleRestApiTest extends BaseWebScriptTest implements RecordsManagem finally { // Clean up - rmSecurityService.deleteRole(rmRootNode, role1); + securityService.deleteRole(filePlan, role1); } } @@ -268,11 +230,11 @@ public class RoleRestApiTest extends BaseWebScriptTest implements RecordsManagem public void testDeleteRole() throws Exception { String role1 = GUID.generate(); - assertFalse(rmSecurityService.existsRole(rmRootNode, role1)); - rmSecurityService.createRole(rmRootNode, role1, "My Test Role", getListOfCapabilities(5)); - assertTrue(rmSecurityService.existsRole(rmRootNode, role1)); + assertFalse(securityService.existsRole(filePlan, role1)); + securityService.createRole(filePlan, role1, "My Test Role", getListOfCapabilities(5)); + assertTrue(securityService.existsRole(filePlan, role1)); sendRequest(new DeleteRequest(GET_ROLES_URL + "/" + role1),200); - assertFalse(rmSecurityService.existsRole(rmRootNode, role1)); + assertFalse(securityService.existsRole(filePlan, role1)); // Bad request sendRequest(new DeleteRequest(GET_ROLES_URL + "/cheese"), 404); @@ -286,7 +248,7 @@ public class RoleRestApiTest extends BaseWebScriptTest implements RecordsManagem private Set getListOfCapabilities(int size, int offset) { Set result = new HashSet(size); - Set caps = rmSecurityService.getCapabilities(); + Set caps = securityService.getCapabilities(); int count = 0; for (Capability cap : caps) { From 9d7fe7fd4a75f7cf81eb7e3327df887fc1557a14 Mon Sep 17 00:00:00 2001 From: Roy Wetherall Date: Mon, 16 Apr 2012 02:59:54 +0000 Subject: [PATCH 19/24] RM Bug Fixes: * Fixed up issues seen during QA security knowledge transfer session * hard coded "Read" evaluation in DocLib js is overridden in RM to account for "ReadRecord" ... not ideal solution but no other option for the moment * property pages now show for non-admin users * actions on toolbar showing and hidding correctly when capabilities missing * other actions showing and hiding correctly when capabilities missing * view details UI action to capability link now working correctly * some unit test monkeying * gradle scripts have 'explodedDeploy' taget which does deploys content of AMP (or at least what would be the contents of the AMP) to the exploded web apps ... speeds up dev time heaps! git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/modules/recordsmanagement/HEAD@35251 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- build.gradle | 52 +- rm-server/.classpath | 2 +- rm-server/build.gradle | 3 - .../rm-capabilities-context.xml | 17 +- .../documentlibrary-v2/doclist.lib.js | 264 +++++ .../slingshot/documentlibrary-v2/node.get.js | 77 ++ .../action/RMActionExecuterAbstractBase.java | 2 +- .../{impl => }/AbstractCapability.java | 7 +- .../declarative/DeclarativeCapability.java | 6 +- .../condition/FillingCapabilityCondition.java | 73 +- .../capability/group/DeclareCapability.java | 2 +- .../capability/group/DeleteCapability.java | 2 +- .../capability/group/UpdateCapability.java | 2 +- .../group/UpdatePropertiesCapability.java | 2 +- .../ChangeOrDeleteReferencesCapability.java | 2 +- .../impl/DeleteLinksCapability.java | 2 +- .../impl/EditRecordMetadataCapability.java | 20 +- .../AddModifyEventDatesCapabilityTest.java | 282 ------ ...veRecordsScheduledForCutoffCapability.java | 307 ------ .../capabilities/BaseTestCapabilities.java | 903 ------------------ .../DeclarativeCapabilityTest.java | 5 - .../capabilities/GroupCapabilityTest.java | 163 ++++ 22 files changed, 580 insertions(+), 1615 deletions(-) create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary-v2/doclist.lib.js create mode 100644 rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary-v2/node.get.js rename rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/{impl => }/AbstractCapability.java (91%) delete mode 100644 rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/AddModifyEventDatesCapabilityTest.java delete mode 100644 rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/ApproveRecordsScheduledForCutoffCapability.java delete mode 100644 rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/BaseTestCapabilities.java create mode 100644 rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/GroupCapabilityTest.java diff --git a/build.gradle b/build.gradle index 4faaac8596..bb38b225da 100644 --- a/build.gradle +++ b/build.gradle @@ -15,16 +15,6 @@ task wrapper(type: Wrapper) { gradleVersion = '1.0-milestone-8' } -task downloadAlfresco << { - - def address = "https://bamboo.alfresco.com/bamboo/artifact/ALF-ENTERPRISEV40/JOB1/build-891/ALL/alfresco-enterprise-4.0.1.zip?os_authType=basic&os_username=rwetherall&os_password=31vegaleg" - - def file = new FileOutputStream(file('alfresco.zip')) - def out = new BufferedOutputStream(file) - out << new URL(address).openStream() - out.close() -} - /** Subproject configuration */ subprojects { @@ -50,6 +40,7 @@ subprojects { jarFile = "${baseName}.jar" ampFile = "${baseName}.amp" tomcatRoot = System.getenv(tomcatEnv) + jarFilePath = "${buildLibDir}/${jarFile}" sourceSets { main { @@ -161,7 +152,6 @@ subprojects { task amp(dependsOn: 'jar') << { - def jarFilePath = "${buildLibDir}/${jarFile}" def jarFileObj = file(jarFilePath) def configDirObj = file(configDir) def sourceWebObj = file(sourceWebDir) @@ -197,6 +187,46 @@ subprojects { } } + task deployExploded(dependsOn: 'jar') << { + + def jarFileObj = file(jarFilePath) + def configDirObj = file(configDir) + def sourceWebObj = file(sourceWebDir) + + explodedWebAppDir = new File("${tomcatRoot}/webapps/${webAppName}") + if (explodedWebAppDir.exists() == true) { + + // copy module properties + + // copy jars + if (jarFileObj.exists()) { + copy { + from jarFilePath + into "${explodedWebAppDir}/WEB-INF/lib" + } + } + + // copy config + if (configDirObj.exists() == true) { + copy { + from(configDir) { + exclude "**/${moduleProperties}" + exclude "**/${fileMapping}" + } + into "${explodedWebAppDir}/WEB-INF/classes" + + } + } + + // copy web + if (sourceWebObj.exists() == true) { + } + } + else { + println "Exploded webapp directory ${explodedWebAppDir} does not exist." + } + } + task installAmp(dependsOn: ['amp', 'copyWar']) << { def warFileLocation = file("${buildDistDir}/${warFile}") diff --git a/rm-server/.classpath b/rm-server/.classpath index 10f3e73b1d..e2ff39fa0f 100644 --- a/rm-server/.classpath +++ b/rm-server/.classpath @@ -249,7 +249,7 @@ - + diff --git a/rm-server/build.gradle b/rm-server/build.gradle index 75520e3ae5..0008684d53 100644 --- a/rm-server/build.gradle +++ b/rm-server/build.gradle @@ -20,7 +20,4 @@ test { beforeTest { descriptor -> logger.lifecycle("Running test: " + descriptor) } - //onOutput { descriptor, event -> - // logger.lifecycle(event.message) - //} } \ No newline at end of file diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-capabilities-context.xml b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-capabilities-context.xml index dce641860e..29354fe42d 100644 --- a/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-capabilities-context.xml +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-capabilities-context.xml @@ -48,7 +48,6 @@ - RECORD_CATEGORY DISPOSITION_SCHEDULE - + + + + + + - - - + RECORD @@ -827,7 +830,9 @@ - + diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary-v2/doclist.lib.js b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary-v2/doclist.lib.js new file mode 100644 index 0000000000..947666762f --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary-v2/doclist.lib.js @@ -0,0 +1,264 @@ +const REQUEST_MAX = 1000; + +/** + * Main entry point: Create collection of documents and folders in the given space + * + * @method doclist_main + */ +function doclist_main() +{ + // Use helper function to get the arguments + var parsedArgs = ParseArgs.getParsedArgs(); + if (parsedArgs === null) + { + return; + } + + var filter = args.filter, + items = []; + + // Try to find a filter query based on the passed-in arguments + var allNodes = [], + totalRecords = 0, + requestTotalCountMax = 0, + paged = false, + favourites = Common.getFavourites(), + filterParams = Filters.getFilterParams(filter, parsedArgs, + { + favourites: favourites + }), + query = filterParams.query; + + if ((filter || "path") == "path") + { + // TODO also add DB filter by "node" (in addition to "path") + var parentNode = parsedArgs.pathNode; + if (parentNode !== null) + { + var skip = -1, + max = -1; + + if (args.size != null) + { + max = args.size; + + if (args.pos > 0) + { + skip = (args.pos - 1) * max; + } + } + + var sortField = (args.sortField == null ? "cm:name" : args.sortField), + sortAsc = (((args.sortAsc == null) || (args.sortAsc == "true")) ? true : false); + + // Get paged set + requestTotalCountMax = skip + REQUEST_MAX; + var pagedResult = parentNode.childFileFolders(true, true, filterParams.ignoreTypes, skip, max, requestTotalCountMax, sortField, sortAsc, "TODO"); + + allNodes = pagedResult.page; + totalRecords = pagedResult.totalResultCountUpper; + paged = true; + } + } + else + { + // Query the nodes - passing in sort and result limit parameters + if (query !== "") + { + allNodes = search.query( + { + query: query, + language: filterParams.language, + page: + { + maxItems: (filterParams.limitResults ? parseInt(filterParams.limitResults, 10) : 0) + }, + sort: filterParams.sort, + templates: filterParams.templates, + namespace: (filterParams.namespace ? filterParams.namespace : null) + }); + + totalRecords = allNodes.length; + } + } + + // Ensure folders and folderlinks appear at the top of the list + var folderNodes = [], + documentNodes = []; + + for each (node in allNodes) + { + try + { + if (node.isContainer || node.isLinkToContainer) + { + folderNodes.push(node); + } + else + { + documentNodes.push(node); + } + } + catch (e) + { + // Possibly an old indexed node - ignore it + } + } + + // Node type counts + var folderNodesCount = folderNodes.length, + documentNodesCount = documentNodes.length, + nodes; + + if (parsedArgs.type === "documents") + { + nodes = documentNodes; + totalRecords -= folderNodesCount; + } + else + { + // TODO: Sorting with folders at end -- swap order of concat() + nodes = folderNodes.concat(documentNodes); + } + + // Pagination + var pageSize = args.size || nodes.length, + pagePos = args.pos || "1", + startIndex = (pagePos - 1) * pageSize; + + if (!paged) + { + // Trim the nodes array down to the page size + nodes = nodes.slice(startIndex, pagePos * pageSize); + } + + // Common or variable parent container? + var parent = null; + + if (!filterParams.variablePath) + { + // Parent node permissions (and Site role if applicable) + parent = Evaluator.run(parsedArgs.pathNode, true); + } + + var isThumbnailNameRegistered = thumbnailService.isThumbnailNameRegistered(THUMBNAIL_NAME), + thumbnail = null, + locationNode, + item; + + // Loop through and evaluate each node in this result set + for each (node in nodes) + { + // Get evaluated properties. + item = Evaluator.run(node); + 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) + { + locationNode = item.isLink ? item.linkedNode : item.node; + location = Common.getLocation(locationNode, parsedArgs.libraryRoot); + // Parent node + if (node.parent != null && + (node.parent.hasPermission("Read") || node.parent.hasPermission("ReadRecords"))) + { + item.parent = Evaluator.run(node.parent, true); + } + } + else + { + location = + { + site: parsedArgs.location.site, + siteTitle: parsedArgs.location.siteTitle, + sitePreset: parsedArgs.location.sitePreset, + container: parsedArgs.location.container, + containerType: parsedArgs.location.containerType, + path: parsedArgs.location.path, + file: node.name + }; + } + + // Resolved location + item.location = location; + + // Check: thumbnail type is registered && node is a cm:content subtype && valid inputStream for content property + if (isThumbnailNameRegistered && item.node.isSubType("cm:content") && item.node.properties.content.inputStream != null) + { + // Make sure we have a thumbnail. + thumbnail = item.node.getThumbnail(THUMBNAIL_NAME); + if (thumbnail === null) + { + // No thumbnail, so queue creation + item.node.createThumbnail(THUMBNAIL_NAME, true); + } + } + + items.push(item); + } + else + { + --totalRecords; + } + } + + // Array Remove - By John Resig (MIT Licensed) + var fnArrayRemove = function fnArrayRemove(array, from, to) + { + var rest = array.slice((to || from) + 1 || array.length); + array.length = from < 0 ? array.length + from : from; + return array.push.apply(array, rest); + }; + + /** + * De-duplicate orignals for any existing working copies. + * This can't be done in evaluator.lib.js as it has no knowledge of the current filter or UI operation. + * Note: This may result in pages containing less than the configured amount of items (50 by default). + */ + for each (item in items) + { + if (item.workingCopy.isWorkingCopy) + { + var workingCopySource = String(item.workingCopy.sourceNodeRef); + for (var i = 0, ii = items.length; i < ii; i++) + { + if (String(items[i].node.nodeRef) == workingCopySource) + { + fnArrayRemove(items, i); + --totalRecords; + break; + } + } + } + } + + var paging = + { + totalRecords: totalRecords, + startIndex: startIndex + }; + + if (paged && (totalRecords == requestTotalCountMax)) + { + paging.totalRecordsUpper = requestTotalCountMax; + } + + return ( + { + luceneQuery: query, + paging: paging, + container: parsedArgs.rootNode, + parent: parent, + onlineEditing: utils.moduleInstalled("org.alfresco.module.vti"), + itemCount: + { + folders: folderNodesCount, + documents: documentNodesCount + }, + items: items, + customJSON: slingshotDocLib.getJSON() + }); +} diff --git a/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary-v2/node.get.js b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary-v2/node.get.js new file mode 100644 index 0000000000..145314fb8c --- /dev/null +++ b/rm-server/config/alfresco/templates/webscripts/org/alfresco/slingshot/documentlibrary-v2/node.get.js @@ -0,0 +1,77 @@ + + + +/** + * Main entry point: Return single document or folder given it's nodeRef + * + * @method getDoclist + */ +function getDoclist() +{ + // Use helper function to get the arguments + var parsedArgs = ParseArgs.getParsedArgs(); + if (parsedArgs === null) + { + return; + } + + parsedArgs.pathNode = ParseArgs.resolveNode(parsedArgs.nodeRef); + parsedArgs.location = Common.getLocation(parsedArgs.pathNode, parsedArgs.libraryRoot); + + var favourites = Common.getFavourites(), + node = parsedArgs.pathNode; + + var isThumbnailNameRegistered = thumbnailService.isThumbnailNameRegistered(THUMBNAIL_NAME), + thumbnail = null, + item = Evaluator.run(node); + + item.isFavourite = (favourites[node.nodeRef] === true); + item.likes = Common.getLikes(node); + item.location = + { + site: parsedArgs.location.site, + siteTitle: parsedArgs.location.siteTitle, + container: parsedArgs.location.container, + containerType: parsedArgs.location.containerType, + path: parsedArgs.location.path, + file: node.name + }; + + item.parent = {}; + if (node.parent != null && (node.parent.hasPermission("Read") || node.parent.hasPermission("ReadRecords"))) + { + item.parent = Evaluator.run(node.parent, true); + } + + // Special case for container and libraryRoot nodes + if ((parsedArgs.location.containerNode && String(parsedArgs.location.containerNode.nodeRef) == String(node.nodeRef)) || + (parsedArgs.libraryRoot && String(parsedArgs.libraryRoot.nodeRef) == String(node.nodeRef))) + { + item.location.file = ""; + } + + // Check: thumbnail type is registered && node is a cm:content subtype && valid inputStream for content property + if (isThumbnailNameRegistered && item.node.isSubType("cm:content") && item.node.properties.content.inputStream != null) + { + // Make sure we have a thumbnail. + thumbnail = item.node.getThumbnail(THUMBNAIL_NAME); + if (thumbnail === null) + { + // No thumbnail, so queue creation + item.node.createThumbnail(THUMBNAIL_NAME, true); + } + } + + return ( + { + container: parsedArgs.rootNode, + onlineEditing: utils.moduleInstalled("org.alfresco.module.vti"), + item: item, + customJSON: slingshotDocLib.getJSON() + }); +} + +/** + * Document List Component: doclist + */ +model.doclist = getDoclist(); \ No newline at end of file diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/RMActionExecuterAbstractBase.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/RMActionExecuterAbstractBase.java index 92543eaf4b..9ab72d3ae2 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/RMActionExecuterAbstractBase.java +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/RMActionExecuterAbstractBase.java @@ -31,7 +31,7 @@ import java.util.Set; import org.alfresco.module.org_alfresco_module_rm.RecordsManagementAdminService; import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; import org.alfresco.module.org_alfresco_module_rm.audit.RecordsManagementAuditService; -import org.alfresco.module.org_alfresco_module_rm.capability.impl.AbstractCapability; +import org.alfresco.module.org_alfresco_module_rm.capability.AbstractCapability; import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionAction; import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionActionDefinition; import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule; diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/AbstractCapability.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/AbstractCapability.java similarity index 91% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/AbstractCapability.java rename to rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/AbstractCapability.java index 081d5db447..5ad0a2c4fa 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/AbstractCapability.java +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/AbstractCapability.java @@ -16,7 +16,7 @@ * You should have received a copy of the GNU Lesser General Public License * along with Alfresco. If not, see . */ -package org.alfresco.module.org_alfresco_module_rm.capability.impl; +package org.alfresco.module.org_alfresco_module_rm.capability; import java.util.ArrayList; import java.util.List; @@ -24,11 +24,6 @@ import java.util.List; import net.sf.acegisecurity.vote.AccessDecisionVoter; import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction; -import org.alfresco.module.org_alfresco_module_rm.capability.Capability; -import org.alfresco.module.org_alfresco_module_rm.capability.CapabilityService; -import org.alfresco.module.org_alfresco_module_rm.capability.RMEntryVoter; -import org.alfresco.module.org_alfresco_module_rm.capability.RMPermissionModel; -import org.alfresco.module.org_alfresco_module_rm.capability.RMSecurityCommon; import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.security.AccessStatus; diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/DeclarativeCapability.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/DeclarativeCapability.java index 5b9927d1a5..40e1a9ac17 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/DeclarativeCapability.java +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/DeclarativeCapability.java @@ -26,7 +26,7 @@ import net.sf.acegisecurity.vote.AccessDecisionVoter; import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.module.org_alfresco_module_rm.FilePlanComponentKind; -import org.alfresco.module.org_alfresco_module_rm.capability.impl.AbstractCapability; +import org.alfresco.module.org_alfresco_module_rm.capability.AbstractCapability; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.security.AccessStatus; import org.springframework.beans.BeansException; @@ -41,7 +41,7 @@ import org.springframework.context.ApplicationContextAware; public class DeclarativeCapability extends AbstractCapability implements ApplicationContextAware { /** Application Context */ - private ApplicationContext applicationContext; + protected ApplicationContext applicationContext; /** Required permissions */ private List permissions; @@ -233,7 +233,7 @@ public class DeclarativeCapability extends AbstractCapability implements Applica } /** - * @see org.alfresco.module.org_alfresco_module_rm.capability.impl.AbstractCapability#hasPermissionImpl(org.alfresco.service.cmr.repository.NodeRef) + * @see org.alfresco.module.org_alfresco_module_rm.capability.AbstractCapability#hasPermissionImpl(org.alfresco.service.cmr.repository.NodeRef) */ @Override public int evaluate(NodeRef nodeRef) diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/FillingCapabilityCondition.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/FillingCapabilityCondition.java index 61366a9629..f199155f3e 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/FillingCapabilityCondition.java +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/FillingCapabilityCondition.java @@ -18,14 +18,10 @@ */ package org.alfresco.module.org_alfresco_module_rm.capability.declarative.condition; -import org.alfresco.model.ContentModel; import org.alfresco.module.org_alfresco_module_rm.capability.RMPermissionModel; import org.alfresco.module.org_alfresco_module_rm.capability.declarative.AbstractCapabilityCondition; -import org.alfresco.service.cmr.dictionary.DictionaryService; -import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.security.AccessStatus; -import org.alfresco.service.namespace.QName; /** * Filling capability condition. @@ -34,17 +30,6 @@ import org.alfresco.service.namespace.QName; */ public class FillingCapabilityCondition extends AbstractCapabilityCondition { - /** Dictionary service */ - private DictionaryService dictionaryService; - - /** - * @param dictionaryService dictionary service - */ - public void setDictionaryService(DictionaryService dictionaryService) - { - this.dictionaryService = dictionaryService; - } - /** * @see org.alfresco.module.org_alfresco_module_rm.capability.declarative.CapabilityCondition#evaluate(org.alfresco.service.cmr.repository.NodeRef) */ @@ -53,65 +38,11 @@ public class FillingCapabilityCondition extends AbstractCapabilityCondition { boolean result = false; - NodeRef filePlan = rmService.getFilePlan(nodeRef); - - if (permissionService.hasPermission(filePlan, RMPermissionModel.ROLE_ADMINISTRATOR) == AccessStatus.ALLOWED) + if (permissionService.hasPermission(nodeRef, RMPermissionModel.FILE_RECORDS) != AccessStatus.DENIED) { result = true; } - else - { - QName nodeType = nodeService.getType(nodeRef); - if (rmService.isRecord(nodeRef) == true || - dictionaryService.isSubClass(nodeType, ContentModel.TYPE_CONTENT) == true) - { - // Multifiling - if you have filing rights to any of the folders in which the record resides - // then you have filing rights. - for (ChildAssociationRef car : nodeService.getParentAssocs(nodeRef)) - { - if (car != null) - { - if (permissionService.hasPermission(car.getParentRef(), RMPermissionModel.FILE_RECORDS) == AccessStatus.ALLOWED) - { - result = true; - break; - } - } - } - } - else if (rmService.isRecordFolder(nodeRef) == true) - { - if (permissionService.hasPermission(nodeRef, RMPermissionModel.FILE_RECORDS) != AccessStatus.DENIED) - { - result = true; - } - } - else if (rmService.isRecordCategory(nodeRef) == true) - { - if (permissionService.hasPermission(nodeRef, RMPermissionModel.FILE_RECORDS) != AccessStatus.DENIED) - { - result = true; - } - else if (permissionService.hasPermission(filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS) != AccessStatus.DENIED) - { - result = true; - } - } - // else other file plan component - else - { - if (permissionService.hasPermission(nodeRef, RMPermissionModel.FILE_RECORDS) != AccessStatus.DENIED) - { - result = true; - } - else if (permissionService.hasPermission(filePlan, RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA) != AccessStatus.DENIED) - { - result = true; - } - } - - } - return result; + return result; } } diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/DeclareCapability.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/DeclareCapability.java index 9e696f855a..060b5f83c6 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/DeclareCapability.java +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/DeclareCapability.java @@ -20,9 +20,9 @@ package org.alfresco.module.org_alfresco_module_rm.capability.group; import net.sf.acegisecurity.vote.AccessDecisionVoter; +import org.alfresco.module.org_alfresco_module_rm.capability.AbstractCapability; import org.alfresco.module.org_alfresco_module_rm.capability.Capability; import org.alfresco.module.org_alfresco_module_rm.capability.RMPermissionModel; -import org.alfresco.module.org_alfresco_module_rm.capability.impl.AbstractCapability; import org.alfresco.service.cmr.repository.NodeRef; /** diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/DeleteCapability.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/DeleteCapability.java index 2af89a535a..9b7395d851 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/DeleteCapability.java +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/DeleteCapability.java @@ -20,9 +20,9 @@ package org.alfresco.module.org_alfresco_module_rm.capability.group; import net.sf.acegisecurity.vote.AccessDecisionVoter; +import org.alfresco.module.org_alfresco_module_rm.capability.AbstractCapability; import org.alfresco.module.org_alfresco_module_rm.capability.Capability; import org.alfresco.module.org_alfresco_module_rm.capability.RMPermissionModel; -import org.alfresco.module.org_alfresco_module_rm.capability.impl.AbstractCapability; import org.alfresco.service.cmr.repository.NodeRef; /** diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/UpdateCapability.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/UpdateCapability.java index 39160cd70b..671dad38a9 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/UpdateCapability.java +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/UpdateCapability.java @@ -23,8 +23,8 @@ import java.util.Map; import net.sf.acegisecurity.vote.AccessDecisionVoter; +import org.alfresco.module.org_alfresco_module_rm.capability.AbstractCapability; import org.alfresco.module.org_alfresco_module_rm.capability.Capability; -import org.alfresco.module.org_alfresco_module_rm.capability.impl.AbstractCapability; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.namespace.QName; diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/UpdatePropertiesCapability.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/UpdatePropertiesCapability.java index cf36f4909a..b1423fcd29 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/UpdatePropertiesCapability.java +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/UpdatePropertiesCapability.java @@ -23,8 +23,8 @@ import java.util.Map; import net.sf.acegisecurity.vote.AccessDecisionVoter; +import org.alfresco.module.org_alfresco_module_rm.capability.AbstractCapability; import org.alfresco.module.org_alfresco_module_rm.capability.Capability; -import org.alfresco.module.org_alfresco_module_rm.capability.impl.AbstractCapability; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.namespace.QName; diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/ChangeOrDeleteReferencesCapability.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/ChangeOrDeleteReferencesCapability.java index 121cf3b263..1d7143be12 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/ChangeOrDeleteReferencesCapability.java +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/ChangeOrDeleteReferencesCapability.java @@ -41,7 +41,7 @@ public class ChangeOrDeleteReferencesCapability extends DeclarativeCapability } /** - * @see org.alfresco.module.org_alfresco_module_rm.capability.impl.AbstractCapability#evaluate(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.cmr.repository.NodeRef) + * @see org.alfresco.module.org_alfresco_module_rm.capability.AbstractCapability#evaluate(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.cmr.repository.NodeRef) */ public int evaluate(NodeRef source, NodeRef target) { diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/DeleteLinksCapability.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/DeleteLinksCapability.java index 20273a3e43..75d7347ae2 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/DeleteLinksCapability.java +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/DeleteLinksCapability.java @@ -41,7 +41,7 @@ public class DeleteLinksCapability extends DeclarativeCapability } /** - * @see org.alfresco.module.org_alfresco_module_rm.capability.impl.AbstractCapability#evaluate(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.cmr.repository.NodeRef) + * @see org.alfresco.module.org_alfresco_module_rm.capability.AbstractCapability#evaluate(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.cmr.repository.NodeRef) */ public int evaluate(NodeRef source, NodeRef target) { diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/EditRecordMetadataCapability.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/EditRecordMetadataCapability.java index aaa806b263..e93f534173 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/EditRecordMetadataCapability.java +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/EditRecordMetadataCapability.java @@ -31,17 +31,17 @@ import org.alfresco.service.cmr.security.OwnableService; * * @author Roy Wetherall */ -public class EditRecordMetadataCapability extends DeclarativeCapability +public class EditRecordMetadataCapability extends DeclarativeCapability { - /** Ownable service */ private OwnableService ownableService; - - /** - * @param ownableService ownable service - */ - public void setOwnableService(OwnableService ownableService) + + private OwnableService getOwnableService() { - this.ownableService = ownableService; + if (ownableService == null) + { + ownableService = (OwnableService)applicationContext.getBean("OwnableService"); + } + return ownableService; } /** @@ -76,11 +76,11 @@ public class EditRecordMetadataCapability extends DeclarativeCapability // Since we know this is undeclared if you are the owner then you should be able to // edit the records meta-data (otherwise how can it be declared by the user?) - if (ownableService.hasOwner(nodeRef) == true) + if (getOwnableService().hasOwner(nodeRef) == true) { String user = AuthenticationUtil.getFullyAuthenticatedUser(); if (user != null && - ownableService.getOwner(nodeRef).equals(user) == true) + getOwnableService().getOwner(nodeRef).equals(user) == true) { result = Integer.valueOf(AccessDecisionVoter.ACCESS_GRANTED); } diff --git a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/AddModifyEventDatesCapabilityTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/AddModifyEventDatesCapabilityTest.java deleted file mode 100644 index 4d2dba2b8d..0000000000 --- a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/AddModifyEventDatesCapabilityTest.java +++ /dev/null @@ -1,282 +0,0 @@ -/* - * 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.module.org_alfresco_module_rm.test.capabilities; - -import java.io.Serializable; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; - -import org.alfresco.module.org_alfresco_module_rm.action.impl.CompleteEventAction; -import org.alfresco.module.org_alfresco_module_rm.action.impl.FreezeAction; -import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; -import org.alfresco.repo.security.authentication.AuthenticationUtil; -import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; -import org.alfresco.service.cmr.security.AccessStatus; - -/** - * @author Roy Wetherall - */ -public class AddModifyEventDatesCapabilityTest extends BaseTestCapabilities -{ - /** - * - * @throws Exception - */ - public void testAddModifyEventDatesCapability() throws Exception - { - // Check file plan permissions - checkPermissions( - filePlan, - ADD_MODIFY_EVENT_DATES, - stdUsers, - new AccessStatus[] - { - AccessStatus.ALLOWED, - AccessStatus.ALLOWED, - AccessStatus.ALLOWED, - AccessStatus.ALLOWED, - AccessStatus.ALLOWED, - AccessStatus.DENIED - }); - - checkCapabilities( - recordFolder_1, - ADD_MODIFY_EVENT_DATES, - stdUsers, - new AccessStatus[] - { - AccessStatus.ALLOWED, - AccessStatus.ALLOWED, - AccessStatus.ALLOWED, - AccessStatus.ALLOWED, - AccessStatus.ALLOWED, - AccessStatus.DENIED - }); - - checkCapabilities( - record_1, - ADD_MODIFY_EVENT_DATES, - stdUsers, - new AccessStatus[] - { - AccessStatus.DENIED, - AccessStatus.DENIED, - AccessStatus.DENIED, - AccessStatus.DENIED, - AccessStatus.DENIED, - AccessStatus.DENIED - }); - - checkCapabilities( - recordFolder_2, - ADD_MODIFY_EVENT_DATES, - stdUsers, - new AccessStatus[] - { - AccessStatus.DENIED, - AccessStatus.DENIED, - AccessStatus.DENIED, - AccessStatus.DENIED, - AccessStatus.DENIED, - AccessStatus.DENIED - }); - - checkCapabilities( - record_2, - ADD_MODIFY_EVENT_DATES, - stdUsers, - new AccessStatus[] - { - AccessStatus.ALLOWED, - AccessStatus.ALLOWED, - AccessStatus.ALLOWED, - AccessStatus.ALLOWED, - AccessStatus.ALLOWED, - AccessStatus.DENIED - }); - - /** Test user has no capabilities */ - checkTestUserCapabilities(ADD_MODIFY_EVENT_DATES, - AccessStatus.DENIED, // recordFolder_1 - AccessStatus.DENIED, // record_1 - AccessStatus.DENIED, // recordFolder_2 - AccessStatus.DENIED); // record_2 - - /** Add filing to both record folders */ - retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() - { - @Override - public Object execute() throws Throwable - { - AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.SYSTEM_USER_NAME); - - permissionService.setPermission(filePlan, testers, VIEW_RECORDS, true); - permissionService.setInheritParentPermissions(recordCategory_1, false); - permissionService.setInheritParentPermissions(recordCategory_2, false); - permissionService.setPermission(recordCategory_1, testers, READ_RECORDS, true); - permissionService.setPermission(recordCategory_2, testers, READ_RECORDS, true); - permissionService.setPermission(recordFolder_1, testers, FILING, true); - permissionService.setPermission(recordFolder_2, testers, FILING, true); - - return null; - } - }, false, true); - - /** Check capabilities */ - checkTestUserCapabilities(ADD_MODIFY_EVENT_DATES, - AccessStatus.DENIED, // recordFolder_1 - AccessStatus.DENIED, // record_1 - AccessStatus.DENIED, // recordFolder_2 - AccessStatus.DENIED); // record_2 - - /** Add declare record capability */ - addCapability(DECLARE_RECORDS, testers, filePlan); - checkTestUserCapabilities(ADD_MODIFY_EVENT_DATES, - AccessStatus.DENIED, // recordFolder_1 - AccessStatus.DENIED, // record_1 - AccessStatus.DENIED, // recordFolder_2 - AccessStatus.DENIED); // record_2 - - /** Add modify event date capability */ - addCapability(ADD_MODIFY_EVENT_DATES, testers, filePlan); - checkTestUserCapabilities(ADD_MODIFY_EVENT_DATES, - AccessStatus.ALLOWED, // recordFolder_1 - AccessStatus.DENIED, // record_1 - AccessStatus.DENIED, // recordFolder_2 - AccessStatus.ALLOWED); // record_2 - - /** Remove declare capability */ - removeCapability(DECLARE_RECORDS, testers, filePlan); - checkTestUserCapabilities(ADD_MODIFY_EVENT_DATES, - AccessStatus.ALLOWED, // recordFolder_1 - AccessStatus.DENIED, // record_1 - AccessStatus.DENIED, // recordFolder_2 - AccessStatus.ALLOWED); // record_2 - - /** Add declare capability */ - addCapability(DECLARE_RECORDS, testers, filePlan); - checkTestUserCapabilities(ADD_MODIFY_EVENT_DATES, - AccessStatus.ALLOWED, // recordFolder_1 - AccessStatus.DENIED, // record_1 - AccessStatus.DENIED, // recordFolder_2 - AccessStatus.ALLOWED); // record_2 - - /** Remove view records capability */ - removeCapability(VIEW_RECORDS, testers, filePlan); - checkTestUserCapabilities(ADD_MODIFY_EVENT_DATES, - AccessStatus.DENIED, // recordFolder_1 - AccessStatus.DENIED, // record_1 - AccessStatus.DENIED, // recordFolder_2 - AccessStatus.DENIED); // record_2 - - /** Add view records capability */ - addCapability(VIEW_RECORDS, testers, filePlan); - checkTestUserCapabilities(ADD_MODIFY_EVENT_DATES, - AccessStatus.ALLOWED, // recordFolder_1 - AccessStatus.DENIED, // record_1 - AccessStatus.DENIED, // recordFolder_2 - AccessStatus.ALLOWED); // record_2 - - /** Remove filing from record folders */ - removeCapability(FILING, testers, recordFolder_1, recordFolder_2); - checkTestUserCapabilities(ADD_MODIFY_EVENT_DATES, - AccessStatus.DENIED, // recordFolder_1 - AccessStatus.DENIED, // record_1 - AccessStatus.DENIED, // recordFolder_2 - AccessStatus.DENIED); // record_2 - - /** Set filing permission on records folders */ - addCapability(FILING, testers, recordFolder_1, recordFolder_2); - checkTestUserCapabilities(ADD_MODIFY_EVENT_DATES, - AccessStatus.ALLOWED, // recordFolder_1 - AccessStatus.DENIED, // record_1 - AccessStatus.DENIED, // recordFolder_2 - AccessStatus.ALLOWED); // record_2 - - /** Freeze folder 1 */ - Map params = new HashMap(1); - params.put(FreezeAction.PARAM_REASON, "one"); - executeAction("freeze", params, recordFolder_1); - checkTestUserCapabilities(ADD_MODIFY_EVENT_DATES, - AccessStatus.DENIED, // recordFolder_1 - AccessStatus.DENIED, // record_1 - AccessStatus.DENIED, // recordFolder_2 - AccessStatus.ALLOWED); // record_2 - - /** Freeze record_2 */ - params = new HashMap(1); - params.put(FreezeAction.PARAM_REASON, "Two"); - executeAction("freeze", params, record_2); - - checkTestUserCapabilities(ADD_MODIFY_EVENT_DATES, - AccessStatus.DENIED, // recordFolder_1 - AccessStatus.DENIED, // record_1 - AccessStatus.DENIED, // recordFolder_2 - AccessStatus.DENIED); // record_2 - - /** Unfreeze */ - executeAction("unfreeze", recordFolder_1, record_2); - checkTestUserCapabilities(ADD_MODIFY_EVENT_DATES, - AccessStatus.ALLOWED, // recordFolder_1 - AccessStatus.DENIED, // record_1 - AccessStatus.DENIED, // recordFolder_2 - AccessStatus.ALLOWED); // record_2 - - /** Close record folders */ - executeAction("closeRecordFolder", recordFolder_1, recordFolder_2); - checkTestUserCapabilities(ADD_MODIFY_EVENT_DATES, - AccessStatus.ALLOWED, // recordFolder_1 - AccessStatus.DENIED, // record_1 - AccessStatus.DENIED, // recordFolder_2 - AccessStatus.ALLOWED); // record_2 - - /** Open record folders */ - executeAction("openRecordFolder", recordFolder_1, recordFolder_2); - checkTestUserCapabilities(ADD_MODIFY_EVENT_DATES, - AccessStatus.ALLOWED, // recordFolder_1 - AccessStatus.DENIED, // record_1 - AccessStatus.DENIED, // recordFolder_2 - AccessStatus.ALLOWED); // record_2 - - /** Try and complete events*/ - Map eventDetails = new HashMap(3); - eventDetails.put(CompleteEventAction.PARAM_EVENT_NAME, "event"); - eventDetails.put(CompleteEventAction.PARAM_EVENT_COMPLETED_AT, new Date()); - eventDetails.put(CompleteEventAction.PARAM_EVENT_COMPLETED_BY, test_user); - executeAction("completeEvent", eventDetails, test_user, recordFolder_1); - checkExecuteActionFail("completeEvent", eventDetails, test_user, recordFolder_2); - checkExecuteActionFail("completeEvent", eventDetails, test_user, record_1); - executeAction("completeEvent", eventDetails, test_user, record_2); - - /** Check properties can not be set */ - checkSetPropertyFail(record_1, RecordsManagementModel.PROP_EVENT_EXECUTION_COMPLETE, test_user, true); - checkSetPropertyFail(record_1, RecordsManagementModel.PROP_EVENT_EXECUTION_COMPLETED_AT, test_user, new Date()); - checkSetPropertyFail(record_1, RecordsManagementModel.PROP_EVENT_EXECUTION_COMPLETED_AT, test_user, "me"); - - /** Declare and cutoff */ - declare(record_1, record_2); - cutoff(recordFolder_1, record_2); - checkTestUserCapabilities(ADD_MODIFY_EVENT_DATES, - AccessStatus.ALLOWED, // recordFolder_1 - AccessStatus.DENIED, // record_1 - AccessStatus.DENIED, // recordFolder_2 - AccessStatus.ALLOWED); // record_2 - } -} diff --git a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/ApproveRecordsScheduledForCutoffCapability.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/ApproveRecordsScheduledForCutoffCapability.java deleted file mode 100644 index 06a6b598ae..0000000000 --- a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/ApproveRecordsScheduledForCutoffCapability.java +++ /dev/null @@ -1,307 +0,0 @@ -/* - * 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.module.org_alfresco_module_rm.test.capabilities; - -import java.io.Serializable; -import java.util.HashMap; -import java.util.Map; - -import org.alfresco.module.org_alfresco_module_rm.action.impl.FreezeAction; -import org.alfresco.repo.security.authentication.AuthenticationUtil; -import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; -import org.alfresco.service.cmr.security.AccessStatus; - -/** - * @author Roy Wetherall - */ -public class ApproveRecordsScheduledForCutoffCapability extends BaseTestCapabilities -{ - public void testApproveRecordsScheduledForCutoffCapability() - { - // File plan permissions - checkPermissions(filePlan, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, stdUsers, - AccessStatus.ALLOWED, - AccessStatus.ALLOWED, - AccessStatus.ALLOWED, - AccessStatus.DENIED, - AccessStatus.DENIED, - AccessStatus.DENIED); - - // Not yet eligible - checkCapabilities(recordFolder_1, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, stdUsers, - AccessStatus.DENIED, - AccessStatus.DENIED, - AccessStatus.DENIED, - AccessStatus.DENIED, - AccessStatus.DENIED, - AccessStatus.DENIED); - checkCapabilities(record_1, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, stdUsers, - AccessStatus.DENIED, - AccessStatus.DENIED, - AccessStatus.DENIED, - AccessStatus.DENIED, - AccessStatus.DENIED, - AccessStatus.DENIED); - checkCapabilities(recordFolder_2, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, stdUsers, - AccessStatus.DENIED, - AccessStatus.DENIED, - AccessStatus.DENIED, - AccessStatus.DENIED, - AccessStatus.DENIED, - AccessStatus.DENIED); - checkCapabilities(record_2, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, stdUsers, - AccessStatus.DENIED, - AccessStatus.DENIED, - AccessStatus.DENIED, - AccessStatus.DENIED, - AccessStatus.DENIED, - AccessStatus.DENIED); - - // Set appropriate state - declare records and make eligible - declare(record_1, record_2); - makeEligible(recordFolder_1, record_2); - - checkCapabilities(recordFolder_1, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, stdUsers, - AccessStatus.ALLOWED, - AccessStatus.ALLOWED, - AccessStatus.ALLOWED, - AccessStatus.DENIED, - AccessStatus.DENIED, - AccessStatus.DENIED); - checkCapabilities(record_1, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, stdUsers, - AccessStatus.DENIED, - AccessStatus.DENIED, - AccessStatus.DENIED, - AccessStatus.DENIED, - AccessStatus.DENIED, - AccessStatus.DENIED); - checkCapabilities(recordFolder_2, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, stdUsers, - AccessStatus.DENIED, - AccessStatus.DENIED, - AccessStatus.DENIED, - AccessStatus.DENIED, - AccessStatus.DENIED, - AccessStatus.DENIED); - checkCapabilities(record_2, APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, stdUsers, - AccessStatus.ALLOWED, - AccessStatus.ALLOWED, - AccessStatus.ALLOWED, - AccessStatus.DENIED, - AccessStatus.DENIED, - AccessStatus.DENIED); - - checkTestUserCapabilities(APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, - AccessStatus.DENIED, // recordFolder_1 - AccessStatus.DENIED, // record_1 - AccessStatus.DENIED, // recordFolder_2 - AccessStatus.DENIED); // record_2 - - retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() - { - @Override - public Object execute() throws Throwable - { - AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.SYSTEM_USER_NAME); - - permissionService.setPermission(filePlan, testers, VIEW_RECORDS, true); - permissionService.setInheritParentPermissions(recordCategory_1, false); - permissionService.setInheritParentPermissions(recordCategory_2, false); - permissionService.setPermission(recordCategory_1, testers, READ_RECORDS, true); - permissionService.setPermission(recordCategory_2, testers, READ_RECORDS, true); - permissionService.setPermission(recordFolder_1, testers, FILING, true); - permissionService.setPermission(recordFolder_2, testers, FILING, true); - - return null; - } - }, false, true); - - checkTestUserCapabilities(APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, - AccessStatus.DENIED, // recordFolder_1 - AccessStatus.DENIED, // record_1 - AccessStatus.DENIED, // recordFolder_2 - AccessStatus.DENIED); // record_2 - - addCapability(DECLARE_RECORDS, testers, filePlan); - checkTestUserCapabilities(APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, - AccessStatus.DENIED, // recordFolder_1 - AccessStatus.DENIED, // record_1 - AccessStatus.DENIED, // recordFolder_2 - AccessStatus.DENIED); // record_2 - - addCapability(APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, testers, filePlan); - checkTestUserCapabilities(APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, - AccessStatus.ALLOWED, // recordFolder_1 - AccessStatus.DENIED, // record_1 - AccessStatus.DENIED, // recordFolder_2 - AccessStatus.ALLOWED); // record_2 - - removeCapability(DECLARE_RECORDS, testers, filePlan); - checkTestUserCapabilities(APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, - AccessStatus.ALLOWED, // recordFolder_1 - AccessStatus.DENIED, // record_1 - AccessStatus.DENIED, // recordFolder_2 - AccessStatus.ALLOWED); // record_2 - - addCapability(DECLARE_RECORDS, testers, filePlan); - checkTestUserCapabilities(APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, - AccessStatus.ALLOWED, // recordFolder_1 - AccessStatus.DENIED, // record_1 - AccessStatus.DENIED, // recordFolder_2 - AccessStatus.ALLOWED); // record_2 - - removeCapability(VIEW_RECORDS, testers, filePlan); - checkTestUserCapabilities(APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, - AccessStatus.DENIED, // recordFolder_1 - AccessStatus.DENIED, // record_1 - AccessStatus.DENIED, // recordFolder_2 - AccessStatus.DENIED); // record_2 - - addCapability(VIEW_RECORDS, testers, filePlan); - checkTestUserCapabilities(APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, - AccessStatus.ALLOWED, // recordFolder_1 - AccessStatus.DENIED, // record_1 - AccessStatus.DENIED, // recordFolder_2 - AccessStatus.ALLOWED); // record_2 - - removeCapability(FILING, testers, recordFolder_1, recordFolder_2); - checkTestUserCapabilities(APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, - AccessStatus.DENIED, // recordFolder_1 - AccessStatus.DENIED, // record_1 - AccessStatus.DENIED, // recordFolder_2 - AccessStatus.DENIED); // record_2 - - addCapability(FILING, testers, recordFolder_1, recordFolder_2); - checkTestUserCapabilities(APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, - AccessStatus.ALLOWED, // recordFolder_1 - AccessStatus.DENIED, // record_1 - AccessStatus.DENIED, // recordFolder_2 - AccessStatus.ALLOWED); // record_2 - - // Freeze record folder - Map params = new HashMap(1); - params.put(FreezeAction.PARAM_REASON, "one"); - executeAction("freeze", params, recordFolder_1); - checkTestUserCapabilities(APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, - AccessStatus.DENIED, // recordFolder_1 - AccessStatus.DENIED, // record_1 - AccessStatus.DENIED, // recordFolder_2 - AccessStatus.ALLOWED); // record_2 - - // Freeze record - executeAction("freeze", params, record_2); - checkTestUserCapabilities(APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, - AccessStatus.DENIED, // recordFolder_1 - AccessStatus.DENIED, // record_1 - AccessStatus.DENIED, // recordFolder_2 - AccessStatus.DENIED); // record_2 - - // Unfreeze - executeAction("unfreeze", recordFolder_1, record_2); - checkTestUserCapabilities(APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, - AccessStatus.ALLOWED, // recordFolder_1 - AccessStatus.DENIED, // record_1 - AccessStatus.DENIED, // recordFolder_2 - AccessStatus.ALLOWED); // record_2 - - // Close folders - executeAction("closeRecordFolder", recordFolder_1, recordFolder_2); - checkTestUserCapabilities(APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, - AccessStatus.ALLOWED, // recordFolder_1 - AccessStatus.DENIED, // record_1 - AccessStatus.DENIED, // recordFolder_2 - AccessStatus.ALLOWED); // record_2 - -// -// AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); -// recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "openRecordFolder"); -// recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "openRecordFolder"); -// -// checkCapability(test_user, recordFolder_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.ALLOWED); -// checkCapability(test_user, record_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); -// checkCapability(test_user, recordFolder_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.DENIED); -// checkCapability(test_user, record_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, AccessStatus.ALLOWED); -// -// // try and cut off -// -// AuthenticationUtil.setFullyAuthenticatedUser(test_user); -// recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "cutoff", null); -// try -// { -// recordsManagementActionService.executeRecordsManagementAction(recordFolder_2, "cutoff", null); -// fail(); -// } -// catch (AccessDeniedException ade) -// { -// -// } -// try -// { -// recordsManagementActionService.executeRecordsManagementAction(record_1, "cutoff", null); -// fail(); -// } -// catch (AccessDeniedException ade) -// { -// -// } -// recordsManagementActionService.executeRecordsManagementAction(record_2, "cutoff", null); -// -// // check protected properties -// -// try -// { -// publicNodeService.setProperty(record_1, RecordsManagementModel.PROP_CUT_OFF_DATE, new Date()); -// fail(); -// } -// catch (AccessDeniedException ade) -// { -// -// } - - // check cutoff again (it is already cut off) - - // try - // { - // recordsManagementActionService.executeRecordsManagementAction(recordFolder_1, "cutoff", null); - // fail(); - // } - // catch (AccessDeniedException ade) - // { - // - // } - // try - // { - // recordsManagementActionService.executeRecordsManagementAction(record_2, "cutoff", null); - // fail(); - // } - // catch (AccessDeniedException ade) - // { - // - // } - - // checkCapability(test_user, recordFolder_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, - // AccessStatus.DENIED); - // checkCapability(test_user, record_1, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, - // AccessStatus.DENIED); - // checkCapability(test_user, recordFolder_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, - // AccessStatus.DENIED); - // checkCapability(test_user, record_2, RMPermissionModel.APPROVE_RECORDS_SCHEDULED_FOR_CUTOFF, - // AccessStatus.DENIED); - } - -} diff --git a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/BaseTestCapabilities.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/BaseTestCapabilities.java deleted file mode 100644 index dc3a734c99..0000000000 --- a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/BaseTestCapabilities.java +++ /dev/null @@ -1,903 +0,0 @@ -/* - * 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.module.org_alfresco_module_rm.test.capabilities; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import javax.transaction.UserTransaction; - -import junit.framework.TestCase; - -import org.alfresco.model.ContentModel; -import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; -import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementActionService; -import org.alfresco.module.org_alfresco_module_rm.capability.Capability; -import org.alfresco.module.org_alfresco_module_rm.capability.CapabilityService; -import org.alfresco.module.org_alfresco_module_rm.capability.RMEntryVoter; -import org.alfresco.module.org_alfresco_module_rm.capability.RMPermissionModel; -import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule; -import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService; -import org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEventService; -import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; -import org.alfresco.module.org_alfresco_module_rm.security.RecordsManagementSecurityService; -import org.alfresco.repo.content.MimetypeMap; -import org.alfresco.repo.security.authentication.AuthenticationUtil; -import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; -import org.alfresco.repo.security.permissions.AccessDeniedException; -import org.alfresco.repo.security.permissions.impl.model.PermissionModel; -import org.alfresco.repo.transaction.RetryingTransactionHelper; -import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; -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; -import org.alfresco.service.cmr.repository.StoreRef; -import org.alfresco.service.cmr.security.AccessStatus; -import org.alfresco.service.cmr.security.AuthorityService; -import org.alfresco.service.cmr.security.AuthorityType; -import org.alfresco.service.cmr.security.PermissionService; -import org.alfresco.service.cmr.security.PersonService; -import org.alfresco.service.namespace.NamespaceService; -import org.alfresco.service.namespace.QName; -import org.alfresco.service.namespace.RegexQNamePattern; -import org.alfresco.service.transaction.TransactionService; -import org.alfresco.util.ApplicationContextHelper; -import org.springframework.context.ApplicationContext; - -/** - * @author Roy Wetherall - */ -public abstract class BaseTestCapabilities extends TestCase - implements RMPermissionModel, RecordsManagementModel -{ - /* Application context */ - protected ApplicationContext ctx; - - /* Root node reference */ - protected StoreRef storeRef; - protected NodeRef rootNodeRef; - - /* Services */ - protected NodeService nodeService; - protected NodeService publicNodeService; - protected TransactionService transactionService; - protected PermissionService permissionService; - protected RecordsManagementService recordsManagementService; - protected RecordsManagementSecurityService recordsManagementSecurityService; - protected RecordsManagementActionService recordsManagementActionService; - protected RecordsManagementEventService recordsManagementEventService; - protected DispositionService dispositionService; - protected CapabilityService capabilityService; - protected PermissionModel permissionModel; - protected ContentService contentService; - protected AuthorityService authorityService; - protected PersonService personService; - protected ContentService publicContentService; - protected RetryingTransactionHelper retryingTransactionHelper; - - protected RMEntryVoter rmEntryVoter; - - protected UserTransaction testTX; - - protected NodeRef filePlan; - protected NodeRef recordSeries; - protected NodeRef recordCategory_1; - protected NodeRef recordCategory_2; - protected NodeRef recordFolder_1; - protected NodeRef recordFolder_2; - protected NodeRef record_1; - protected NodeRef record_2; - protected NodeRef recordCategory_3; - protected NodeRef recordFolder_3; - protected NodeRef record_3; - - // protected String rmUsers; - // protected String rmPowerUsers; - // protected String rmSecurityOfficers; - // protected String rmRecordsManagers; - // protected String rmAdministrators; - - protected String rm_user; - protected String rm_power_user; - protected String rm_security_officer; - protected String rm_records_manager; - protected String rm_administrator; - protected String test_user; - - protected String testers; - - protected String[] stdUsers; - protected NodeRef[] stdNodeRefs;; - - /** - * Test setup - * @throws Exception - */ - protected void setUp() throws Exception - { - // Get the application context - ctx = ApplicationContextHelper.getApplicationContext(); - - // Get beans - nodeService = (NodeService) ctx.getBean("dbNodeService"); - publicNodeService = (NodeService) ctx.getBean("NodeService"); - transactionService = (TransactionService) ctx.getBean("transactionComponent"); - permissionService = (PermissionService) ctx.getBean("permissionService"); - permissionModel = (PermissionModel) ctx.getBean("permissionsModelDAO"); - contentService = (ContentService) ctx.getBean("contentService"); - publicContentService = (ContentService) ctx.getBean("ContentService"); - authorityService = (AuthorityService) ctx.getBean("authorityService"); - personService = (PersonService) ctx.getBean("personService"); - capabilityService = (CapabilityService)ctx.getBean("CapabilityService"); - dispositionService = (DispositionService)ctx.getBean("DispositionService"); - recordsManagementService = (RecordsManagementService) ctx.getBean("RecordsManagementService"); - recordsManagementSecurityService = (RecordsManagementSecurityService) ctx.getBean("RecordsManagementSecurityService"); - recordsManagementActionService = (RecordsManagementActionService) ctx.getBean("RecordsManagementActionService"); - recordsManagementEventService = (RecordsManagementEventService) ctx.getBean("RecordsManagementEventService"); - rmEntryVoter = (RMEntryVoter) ctx.getBean("rmEntryVoter"); - retryingTransactionHelper = (RetryingTransactionHelper)ctx.getBean("retryingTransactionHelper"); - - retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() - { - @Override - public Object execute() throws Throwable - { - // As system user - AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); - - // Create store and get the root node reference - storeRef = nodeService.createStore(StoreRef.PROTOCOL_WORKSPACE, "Test_" + System.currentTimeMillis()); - rootNodeRef = nodeService.getRootNode(storeRef); - - // As admin user - AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); - - // Create test events - recordsManagementEventService.getEvents(); - recordsManagementEventService.addEvent("rmEventType.simple", "event", "My Event"); - - // Create file plan node - filePlan = nodeService.createNode( - rootNodeRef, - ContentModel.ASSOC_CHILDREN, - TYPE_FILE_PLAN, - TYPE_FILE_PLAN).getChildRef(); - - return null; - } - }, false, true); - - - // Load in the plan data required for the test - loadFilePlanData(); - - retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() - { - @Override - public Object execute() throws Throwable - { - // As system user - AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); - - // create people ... - rm_user = "rm_user_" + storeRef.getIdentifier(); - rm_power_user = "rm_power_user_" + storeRef.getIdentifier(); - rm_security_officer = "rm_security_officer_" + storeRef.getIdentifier(); - rm_records_manager = "rm_records_manager_" + storeRef.getIdentifier(); - rm_administrator = "rm_administrator_" + storeRef.getIdentifier(); - test_user = "test_user_" + storeRef.getIdentifier(); - - personService.createPerson(createDefaultProperties(rm_user)); - personService.createPerson(createDefaultProperties(rm_power_user)); - personService.createPerson(createDefaultProperties(rm_security_officer)); - personService.createPerson(createDefaultProperties(rm_records_manager)); - personService.createPerson(createDefaultProperties(rm_administrator)); - personService.createPerson(createDefaultProperties(test_user)); - - // create roles as groups -// rmUsers = authorityService.createAuthority(AuthorityType.GROUP, "RM_USER_" + storeRef.getIdentifier()); -// rmPowerUsers = authorityService.createAuthority(AuthorityType.GROUP, "RM_POWER_USER_" + storeRef.getIdentifier()); -// rmSecurityOfficers = authorityService.createAuthority(AuthorityType.GROUP, "RM_SECURITY_OFFICER_" + storeRef.getIdentifier()); -// rmRecordsManagers = authorityService.createAuthority(AuthorityType.GROUP, "RM_RECORDS_MANAGER_" + storeRef.getIdentifier()); -// rmAdministrators = authorityService.createAuthority(AuthorityType.GROUP, "RM_ADMINISTRATOR_" + storeRef.getIdentifier()); - - testers = authorityService.createAuthority(AuthorityType.GROUP, "RM_TESTOR_" + storeRef.getIdentifier()); - authorityService.addAuthority(testers, test_user); - - // rmUsers = recordsManagementSecurityService.assignRoleToAuthority(filePlan, ROLE, rm_user); - - - setPermissions(rm_user, ROLE_NAME_USER); - setPermissions(rm_power_user, ROLE_NAME_POWER_USER); - setPermissions(rm_security_officer, ROLE_NAME_SECURITY_OFFICER); - setPermissions(rm_records_manager, ROLE_NAME_RECORDS_MANAGER); - setPermissions(rm_administrator, ROLE_NAME_ADMINISTRATOR); - - stdUsers = new String[] - { - AuthenticationUtil.getSystemUserName(), - rm_administrator, - rm_records_manager, - rm_security_officer, - rm_power_user, - rm_user - }; - - stdNodeRefs = new NodeRef[] - { - recordFolder_1, - record_1, - recordFolder_2, - record_2 - }; - - return null; - } - }, false, true); - } - - /** - * Test tear down - * @throws Exception - */ - @Override - protected void tearDown() throws Exception - { - // TODO we should clean up as much as we can .... - } - - /** - * Set the permissions for a group, user and role - * @param group - * @param user - * @param role - */ - private void setPermissions(String user, String role) - { - recordsManagementSecurityService.assignRoleToAuthority(filePlan, role, user); - recordsManagementSecurityService.setPermission(filePlan, user, FILING); - } - - /** - * Loads the file plan date required for the tests - */ - protected void loadFilePlanData() - { - recordSeries = createRecordSeries(filePlan, "RS", "Record Series", "My record series"); - - recordCategory_1 = createRecordCategory(recordSeries, "Docs", "Docs", "Docs", "week|1", true, false); - recordCategory_2 = createRecordCategory(recordSeries, "More Docs", "More Docs", "More Docs", "week|1", true, true); - recordCategory_3 = createRecordSeries(recordSeries, "No Dis", "No disp schedule", "No disp schedule"); - - recordFolder_1 = createRecordFolder(recordCategory_1, "F1", "title", "description"); - recordFolder_2 = createRecordFolder(recordCategory_2, "F2", "title", "description"); - recordFolder_3 = createRecordFolder(recordCategory_3, "F3", "title", "description"); - - record_1 = createRecord(recordFolder_1); - record_2 = createRecord(recordFolder_2); - record_3 = createRecord(recordFolder_3); - } - - /** - * Set permission for authority on node reference. - * @param nodeRef - * @param authority - * @param permission - * @param allow - */ -// private void setPermission(NodeRef nodeRef, String authority, String permission, boolean allow) -// { -// permissionService.setPermission(nodeRef, authority, permission, allow); -// if (permission.equals(FILING)) -// { -// if (recordsManagementService.isRecordCategory(nodeRef) == true) -// { -// List assocs = nodeService.getChildAssocs(nodeRef, ContentModel.ASSOC_CONTAINS, RegexQNamePattern.MATCH_ALL); -// for (ChildAssociationRef assoc : assocs) -// { -// NodeRef child = assoc.getChildRef(); -// if (recordsManagementService.isRecordFolder(child) == true || -// recordsManagementService.isRecordCategory(child) == true) -// { -// setPermission(child, authority, permission, allow); -// } -// } -// } -// } -// } - - /** - * Create the default person properties - * @param userName - * @return - */ - private Map createDefaultProperties(String userName) - { - HashMap properties = new HashMap(); - properties.put(ContentModel.PROP_USERNAME, userName); - properties.put(ContentModel.PROP_HOMEFOLDER, null); - properties.put(ContentModel.PROP_FIRSTNAME, userName); - properties.put(ContentModel.PROP_LASTNAME, userName); - properties.put(ContentModel.PROP_EMAIL, userName); - properties.put(ContentModel.PROP_ORGID, ""); - return properties; - } - - /** - * Create a new record. Executed in a new transaction. - */ - private NodeRef createRecord(final NodeRef recordFolder) - { - return retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() - { - @Override - public NodeRef execute() throws Throwable - { - // As admin - AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); - - // Create the record - Map props = new HashMap(1); - props.put(ContentModel.PROP_NAME, "MyRecord.txt"); - NodeRef recordOne = nodeService.createNode(recordFolder, ContentModel.ASSOC_CONTAINS, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "MyRecord.txt"), - ContentModel.TYPE_CONTENT, props).getChildRef(); - - // Set the content - ContentWriter writer = contentService.getWriter(recordOne, ContentModel.PROP_CONTENT, true); - writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); - writer.setEncoding("UTF-8"); - writer.putContent("There is some content in this record"); - return recordOne; - } - }, false, true); - } - - /** - * Create a test record series. Executed in a new transaction. - */ - private NodeRef createRecordSeries(final NodeRef filePlan, final String name, final String title, final String description) - { - return retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() - { - @Override - public NodeRef execute() throws Throwable - { - // As admin - AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); - - HashMap properties = new HashMap(); - properties.put(ContentModel.PROP_TITLE, title); - properties.put(ContentModel.PROP_DESCRIPTION, description); - - return recordsManagementService.createRecordCategory(filePlan, name, properties); - - } - }, false, true); - } - - /** - * Create a test record category in a new transaction. - */ - private NodeRef createRecordCategory( - final NodeRef recordSeries, - final String name, - final String title, - final String description, - final String review, - final boolean vital, - final boolean recordLevelDisposition) - { - return retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() - { - @Override - public NodeRef execute() throws Throwable - { - // As admin - AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); - - HashMap properties = new HashMap(); - properties.put(ContentModel.PROP_TITLE, title); - properties.put(ContentModel.PROP_DESCRIPTION, description); - - if (vital == true) - { - properties.put(PROP_REVIEW_PERIOD, review); - properties.put(PROP_VITAL_RECORD_INDICATOR, vital); - } - - NodeRef rc = recordsManagementService.createRecordCategory(recordSeries, name, properties); - - properties = new HashMap(); - properties.put(PROP_DISPOSITION_AUTHORITY, "N1-218-00-4 item 023"); - properties.put(PROP_DISPOSITION_INSTRUCTIONS, "Cut off monthly, hold 1 month, then destroy."); - properties.put(PROP_RECORD_LEVEL_DISPOSITION, recordLevelDisposition); - - DispositionSchedule ds = dispositionService.createDispositionSchedule(rc, properties); - - addDispositionAction(ds, "cutoff", "monthend|1", null, "event"); - addDispositionAction(ds, "transfer", "month|1", null, null); - addDispositionAction(ds, "accession", "month|1", null, null); - addDispositionAction(ds, "destroy", "month|1", "{http://www.alfresco.org/model/recordsmanagement/1.0}cutOffDate", null); - - return rc; - } - }, false, true); - } - - /** - * Create disposition action. - * @param disposition - * @param actionName - * @param period - * @param periodProperty - * @param event - * @return - */ - private void addDispositionAction(DispositionSchedule disposition, String actionName, String period, String periodProperty, String event) - { - HashMap properties = new HashMap(); - properties.put(PROP_DISPOSITION_ACTION_NAME, actionName); - properties.put(PROP_DISPOSITION_PERIOD, period); - if (periodProperty != null) - { - properties.put(PROP_DISPOSITION_PERIOD_PROPERTY, periodProperty); - } - if (event != null) - { - properties.put(PROP_DISPOSITION_EVENT, event); - } - dispositionService.addDispositionActionDefinition(disposition, properties); - } - - /** - * Create record folder. Executed in a new transaction. - * @param recordCategory - * @param name - * @param identifier - * @param title - * @param description - * @param review - * @param vital - * @return - */ - private NodeRef createRecordFolder( - final NodeRef recordCategory, - final String name, - final String title, - final String description) - { - return retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() - { - @Override - public NodeRef execute() throws Throwable - { - // As admin - AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); - - HashMap properties = new HashMap(); - properties.put(ContentModel.PROP_TITLE, title); - properties.put(ContentModel.PROP_DESCRIPTION, description); - - return recordsManagementService.createRecordFolder(recordCategory, name, properties); - } - }, false, true); - } - - /** - * - * @param user - * @param nodeRef - * @param capabilityName - * @param accessStstus - */ - protected void checkCapability(final String user, final NodeRef nodeRef, final String capabilityName, final AccessStatus expected) - { - AuthenticationUtil.runAs(new RunAsWork() - { - @Override - public Object doWork() throws Exception - { - Capability capability = recordsManagementSecurityService.getCapability(capabilityName); - assertNotNull(capability); - - List capabilities = new ArrayList(1); - capabilities.add(capabilityName); - Map access = capabilityService.getCapabilitiesAccessState(nodeRef, capabilities); - - AccessStatus actual = access.get(capability); - - assertEquals( - "for user: " + user, - expected, - actual); - - return null; - } - }, user); - } - - /** - * - * @param access - * @param name - * @param accessStatus - */ - protected void check(Map access, String name, AccessStatus accessStatus) - { - Capability capability = recordsManagementSecurityService.getCapability(name); - assertNotNull(capability); - assertEquals(accessStatus, access.get(capability)); - } - - /** - * - * @param user - * @param nodeRef - * @param permission - * @param accessStstus - */ - protected void checkPermission(final String user, final NodeRef nodeRef, final String permission, final AccessStatus accessStstus) - { - AuthenticationUtil.runAs(new RunAsWork() - { - @Override - public Object doWork() throws Exception - { - AccessStatus actualAccessStatus = permissionService.hasPermission(nodeRef, permission); - assertTrue(actualAccessStatus == accessStstus); - return null; - } - }, user); - } - - /** - * - * @param nodeRef - * @param permission - * @param users - * @param expectedAccessStatus - */ - protected void checkPermissions( - final NodeRef nodeRef, - final String permission, - final String[] users, - final AccessStatus ... expectedAccessStatus) - { - retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() - { - @Override - public Object execute() throws Throwable - { - assertEquals( - "The number of users should match the number of expected access status", - users.length, - expectedAccessStatus.length); - - for (int i = 0; i < users.length; i++) - { - checkPermission(users[i], nodeRef, permission, expectedAccessStatus[i]); - } - - return null; - } - }, true, true); - } - - /** - * - * @param nodeRef - * @param capability - * @param users - * @param expectedAccessStatus - */ - protected void checkCapabilities( - final NodeRef nodeRef, - final String capability, - final String[] users, - final AccessStatus ... expectedAccessStatus) - { - retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() - { - @Override - public Object execute() throws Throwable - { - assertEquals( - "The number of users should match the number of expected access status", - users.length, - expectedAccessStatus.length); - - for (int i = 0; i < users.length; i++) - { - checkCapability(users[i], nodeRef, capability, expectedAccessStatus[i]); - } - - return null; - } - }, true, true); - } - - /** - * - * @param user - * @param capability - * @param nodeRefs - * @param expectedAccessStatus - */ - protected void checkCapabilities( - final String user, - final String capability, - final NodeRef[] nodeRefs, - final AccessStatus ... expectedAccessStatus) - { - retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() - { - @Override - public Object execute() throws Throwable - { - assertEquals( - "The number of node references should match the number of expected access status", - nodeRefs.length, - expectedAccessStatus.length); - - for (int i = 0; i < nodeRefs.length; i++) - { - checkCapability(user, nodeRefs[i], capability, expectedAccessStatus[i]); - } - - return null; - } - }, true, true); - } - - /** - * - * @param capability - * @param accessStatus - */ - protected void checkTestUserCapabilities(String capability, AccessStatus ... accessStatus) - { - checkCapabilities( - test_user, - capability, - stdNodeRefs, - accessStatus); - } - - /** - * Execute RM action - * @param action - * @param params - * @param nodeRefs - */ - protected void executeAction(final String action, final Map params, final String user, final NodeRef ... nodeRefs) - { - retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() - { - @Override - public Object execute() throws Throwable - { - AuthenticationUtil.setFullyAuthenticatedUser(user); - - for (NodeRef nodeRef : nodeRefs) - { - recordsManagementActionService.executeRecordsManagementAction(nodeRef, action, params); - } - - return null; - } - }, false, true); - } - - /** - * - * @param action - * @param nodeRefs - */ - protected void executeAction(final String action, final NodeRef ... nodeRefs) - { - executeAction(action, null, AuthenticationUtil.SYSTEM_USER_NAME, nodeRefs); - } - - /** - * - * @param action - * @param params - * @param nodeRefs - */ - protected void executeAction(final String action, final Map params, final NodeRef ... nodeRefs) - { - executeAction(action, params, AuthenticationUtil.SYSTEM_USER_NAME, nodeRefs); - } - - /** - * - * @param action - * @param params - * @param user - * @param nodeRefs - */ - protected void checkExecuteActionFail(final String action, final Map params, final String user, final NodeRef ... nodeRefs) - { - try - { - executeAction(action, params, user, nodeRefs); - fail("Action " + action + " has succeded and was expected to fail"); - } - catch (AccessDeniedException ade) - {} - } - - /** - * - * @param nodeRef - * @param property - * @param user - */ - protected void checkSetPropertyFail(final NodeRef nodeRef, final QName property, final String user, final Serializable value) - { - retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() - { - @Override - public Object execute() throws Throwable - { - AuthenticationUtil.setFullyAuthenticatedUser(user); - - try - { - publicNodeService.setProperty(nodeRef, property, value); - fail("Expected failure when setting property"); - } - catch (AccessDeniedException ade) - {} - - return null; - } - }, false, true); - } - - /** - * Add a capability - * @param capability - * @param authority - * @param nodeRefs - */ - protected void addCapability(final String capability, final String authority, final NodeRef ... nodeRefs) - { - retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() - { - @Override - public Object execute() throws Throwable - { - AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.SYSTEM_USER_NAME); - for (NodeRef nodeRef : nodeRefs) - { - permissionService.setPermission(nodeRef, authority, capability, true); - } - return null; - } - }, false, true); - } - - /** - * Remove capability - * @param capability - * @param authority - * @param nodeRef - */ - protected void removeCapability(final String capability, final String authority, final NodeRef ... nodeRefs) - { - retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() - { - @Override - public Object execute() throws Throwable - { - AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.SYSTEM_USER_NAME); - for (NodeRef nodeRef : nodeRefs) - { - permissionService.deletePermission(nodeRef, authority, capability); - } - return null; - } - }, false, true); - } - - /** - * - * @param nodeRefs - */ - protected void declare(final NodeRef ... nodeRefs) - { - retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() - { - @Override - public Object execute() throws Throwable - { - AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.SYSTEM_USER_NAME); - - for (NodeRef nodeRef : nodeRefs) - { - nodeService.setProperty(nodeRef, RecordsManagementModel.PROP_ORIGINATOR, "origValue"); - nodeService.setProperty(nodeRef, RecordsManagementModel.PROP_ORIGINATING_ORGANIZATION, "origOrgValue"); - nodeService.setProperty(nodeRef, RecordsManagementModel.PROP_PUBLICATION_DATE, new Date()); - nodeService.setProperty(nodeRef, ContentModel.PROP_TITLE, "titleValue"); - recordsManagementActionService.executeRecordsManagementAction(nodeRef, "declareRecord"); - } - - return null; - } - }, false, true); - } - - protected void cutoff(final NodeRef ... nodeRefs) - { - retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() - { - @Override - public Object execute() throws Throwable - { - AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.SYSTEM_USER_NAME); - - Calendar calendar = Calendar.getInstance(); - calendar.set(Calendar.HOUR, 0); - calendar.set(Calendar.MINUTE, 0); - calendar.set(Calendar.SECOND, 0); - - for (NodeRef nodeRef : nodeRefs) - { - NodeRef ndNodeRef = nodeService.getChildAssocs(nodeRef, RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); - nodeService.setProperty(ndNodeRef, RecordsManagementModel.PROP_DISPOSITION_AS_OF, calendar.getTime()); - recordsManagementActionService.executeRecordsManagementAction(nodeRef, "cutoff", null); - } - - return null; - } - }, false, true); - } - - protected void makeEligible(final NodeRef ... nodeRefs) - { - retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() - { - @Override - public Object execute() throws Throwable - { - AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.SYSTEM_USER_NAME); - - Calendar calendar = Calendar.getInstance(); - calendar.set(Calendar.HOUR, 0); - calendar.set(Calendar.MINUTE, 0); - calendar.set(Calendar.SECOND, 0); - - for (NodeRef nodeRef : nodeRefs) - { - NodeRef ndNodeRef = nodeService.getChildAssocs(nodeRef, RecordsManagementModel.ASSOC_NEXT_DISPOSITION_ACTION, RegexQNamePattern.MATCH_ALL).get(0).getChildRef(); - nodeService.setProperty(ndNodeRef, RecordsManagementModel.PROP_DISPOSITION_AS_OF, calendar.getTime()); - } - - return null; - } - }, false, true); - } -} diff --git a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/DeclarativeCapabilityTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/DeclarativeCapabilityTest.java index 2c63548ed6..f20582d61a 100644 --- a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/DeclarativeCapabilityTest.java +++ b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/DeclarativeCapabilityTest.java @@ -237,9 +237,4 @@ public class DeclarativeCapabilityTest extends BaseRMTestCase return result; } - - public void testFrozenCondition() - { - - } } diff --git a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/GroupCapabilityTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/GroupCapabilityTest.java new file mode 100644 index 0000000000..10ac2abccd --- /dev/null +++ b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/GroupCapabilityTest.java @@ -0,0 +1,163 @@ +/* + * 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.module.org_alfresco_module_rm.test.capabilities; + +import org.alfresco.module.org_alfresco_module_rm.capability.Capability; +import org.alfresco.module.org_alfresco_module_rm.capability.RMPermissionModel; +import org.alfresco.module.org_alfresco_module_rm.test.util.BaseRMTestCase; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.AccessStatus; + +/** + * Declarative capability unit test + * + * @author Roy Wetherall + */ +public class GroupCapabilityTest extends BaseRMTestCase +{ + private NodeRef record; + private NodeRef declaredRecord; + + @Override + protected boolean isUserTest() + { + return true; + } + + @Override + protected void setupTestDataImpl() + { + super.setupTestDataImpl(); + + // Pre-filed content + record = utils.createRecord(rmFolder, "record.txt"); + declaredRecord = utils.createRecord(rmFolder, "declaredRecord.txt"); + } + + @Override + protected void setupTestData() + { + super.setupTestData(); + + retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback() + { + @Override + public Object execute() throws Throwable + { + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + + utils.declareRecord(declaredRecord); + + return null; + } + }); + } + + @Override + protected void tearDownImpl() + { + super.tearDownImpl(); + } + + @Override + protected void setupTestUsersImpl(NodeRef filePlan) + { + super.setupTestUsersImpl(filePlan); + + // Give all the users file permission objects + for (String user : testUsers) + { + securityService.setPermission(rmContainer, user, RMPermissionModel.FILING); + } + } + + public void testUpdate() + { + final Capability capability = capabilityService.getCapability("Update"); + assertNotNull(capability); + + doTestInTransaction(new Test() + { + @Override + public Void run() + { + assertEquals(AccessStatus.ALLOWED, capability.hasPermission(rmContainer)); + assertEquals(AccessStatus.ALLOWED, capability.hasPermission(rmFolder)); + assertEquals(AccessStatus.ALLOWED, capability.hasPermission(record)); + assertEquals(AccessStatus.ALLOWED, capability.hasPermission(declaredRecord)); + + return null; + } + }, recordsManagerName); + + doTestInTransaction(new Test() + { + @Override + public Void run() + { + assertEquals(AccessStatus.DENIED, capability.hasPermission(rmContainer)); + assertEquals(AccessStatus.DENIED, capability.hasPermission(rmFolder)); + assertEquals(AccessStatus.DENIED, capability.hasPermission(record)); + assertEquals(AccessStatus.DENIED, capability.hasPermission(declaredRecord)); + + return null; + } + }, userName); + + + } + + public void testUpdateProperties() + { + final Capability capability = capabilityService.getCapability("UpdateProperties"); + assertNotNull(capability); + + doTestInTransaction(new Test() + { + @Override + public Void run() + { + assertEquals(AccessStatus.ALLOWED, capability.hasPermission(rmContainer)); + assertEquals(AccessStatus.ALLOWED, capability.hasPermission(rmFolder)); + assertEquals(AccessStatus.ALLOWED, capability.hasPermission(record)); + assertEquals(AccessStatus.ALLOWED, capability.hasPermission(declaredRecord)); + + return null; + } + }, recordsManagerName); + + doTestInTransaction(new Test() + { + @Override + public Void run() + { + assertEquals(AccessStatus.DENIED, capability.hasPermission(rmContainer)); + assertEquals(AccessStatus.DENIED, capability.hasPermission(rmFolder)); + assertEquals(AccessStatus.DENIED, capability.hasPermission(record)); + assertEquals(AccessStatus.DENIED, capability.hasPermission(declaredRecord)); + + return null; + } + }, userName); + + + } +} From 09a0f50882a8e139e7da7326de45b44d8c85c55b Mon Sep 17 00:00:00 2001 From: Roy Wetherall Date: Wed, 18 Apr 2012 04:58:51 +0000 Subject: [PATCH 20/24] RM Bugs: * Edit details Ui action now reflects user's capabilites correctly * Fixed up a couple of behaviours that don't execute when non-admin user (run as system user since admin may not be rm admin) * Transfers not appear in docLib filter correcetly * File UI action now reflects the user's capability correctly. * Renamed 'group' capabilities to 'private' as this more accurately reflects what it means. * Added composite capability implementation ... allows us to futher consolidate some of the edge cases and will allow us to break down further some of the existing capabilities .. this makes is much easier to see and understand exactlly what each capability is doing * Refactored current 'group' capabilities .. replacing with pure spring config where appropriate .. much clearer what they are doing (and fixed up where they wheren't doing exactlly the right thing) * Moved the remaining group capabilities impl's with the other capability impl's .. we are now down to 8 custom capability implementations .. down from 50+ .. and these havily borrow from the base classes where they can ... makes maintenance MUCH easier! * more unit tests * used new 'private' capability technique to break up FileRecord capability ... it's now clear what it is doing and could be corrected easily git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/modules/recordsmanagement/HEAD@35350 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- .../rm-capabilities-context.xml | 266 ++++++++++-------- .../rm-model-context.xml | 1 + .../rm-ui-evaluators-context.xml | 1 + .../capability/AbstractCapability.java | 16 +- .../capability/Capability.java | 5 +- .../capability/CapabilityService.java | 10 +- .../capability/RMEntryVoter.java | 6 +- .../declarative/CompositeCapability.java | 65 +++++ .../declarative/DeclarativeCapability.java | 4 +- .../DeclaredCapabilityCondition.java | 8 +- .../capability/group/DeclareCapability.java | 54 ---- .../capability/group/DeleteCapability.java | 67 ----- .../capability/group/UpdateCapability.java | 95 ------- .../group/UpdatePropertiesCapability.java | 97 ------- .../group/WriteContentCapability.java | 55 ---- .../{group => impl}/CreateCapability.java | 3 +- .../impl/FileRecordsCapability.java | 115 -------- .../impl/MoveRecordsCapability.java | 1 - .../capability/impl/UpdateCapability.java | 58 ++++ .../impl/UpdatePropertiesCapability.java | 52 ++++ .../model/RecordContainerType.java | 2 +- .../RecordsManagementSecurityServiceImpl.java | 2 +- .../vital/VitalRecordServiceImpl.java | 48 ++-- .../test/CapabilitiesTestSuite.java | 2 + .../test/capabilities/CapabilitiesTest.java | 73 ++--- ...Test.java => CompositeCapabilityTest.java} | 2 +- .../DeclarativeCapabilityTest.java | 52 +++- .../test/util/CommonRMTestUtils.java | 13 + 28 files changed, 482 insertions(+), 691 deletions(-) create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/CompositeCapability.java delete mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/DeclareCapability.java delete mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/DeleteCapability.java delete mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/UpdateCapability.java delete mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/UpdatePropertiesCapability.java delete mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/WriteContentCapability.java rename rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/{group => impl}/CreateCapability.java (95%) delete mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/FileRecordsCapability.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/UpdateCapability.java create mode 100644 rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/UpdatePropertiesCapability.java rename rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/{GroupCapabilityTest.java => CompositeCapabilityTest.java} (96%) diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-capabilities-context.xml b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-capabilities-context.xml index 29354fe42d..0bdbf64c7b 100644 --- a/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-capabilities-context.xml +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-capabilities-context.xml @@ -101,16 +101,24 @@ + + + + + parent="declarativeCapability"> + parent="declarativeCapability"> @@ -129,8 +137,7 @@ + parent="declarativeCapability"> @@ -148,15 +155,13 @@ + parent="declarativeCapability"> + parent="declarativeCapability"> @@ -167,8 +172,7 @@ + parent="declarativeCapability"> @@ -192,8 +196,7 @@ + parent="declarativeCapability"> @@ -212,29 +215,25 @@ + parent="declarativeCapability"> + parent="declarativeCapability"> + parent="declarativeCapability"> + parent="declarativeCapability"> @@ -252,22 +251,20 @@ + parent="declarativeCapability"> + parent="declarativeCapability"> RECORD_CATEGORY RECORD_FOLDER - RECORD + @@ -281,43 +278,37 @@ + parent="declarativeCapability"> + parent="declarativeCapability"> + parent="declarativeCapability"> + parent="declarativeCapability"> + parent="declarativeCapability"> + parent="declarativeCapability"> @@ -337,8 +328,7 @@ + parent="declarativeCapability"> @@ -359,15 +349,13 @@ + parent="declarativeCapability"> + parent="declarativeCapability"> @@ -387,8 +375,7 @@ + parent="declarativeCapability"> @@ -408,8 +395,7 @@ + parent="declarativeCapability"> @@ -429,12 +415,12 @@ + parent="declarativeCapability"> - + + RECORD @@ -446,8 +432,7 @@ + parent="declarativeCapability"> @@ -465,8 +450,7 @@ + parent="declarativeCapability"> @@ -478,15 +462,13 @@ + parent="declarativeCapability"> + parent="declarativeCapability"> @@ -506,8 +488,7 @@ + parent="declarativeCapability"> @@ -539,29 +520,25 @@ + parent="declarativeCapability"> + parent="declarativeCapability"> + parent="declarativeCapability"> + parent="declarativeCapability"> @@ -578,30 +555,46 @@ + + + + + + + + + + + + + + + parent="compositeCapability"> - + + + + + + + parent="declarativeCapability"> + parent="declarativeCapability"> + parent="declarativeCapability"> @@ -612,8 +605,7 @@ + parent="declarativeCapability"> @@ -625,15 +617,13 @@ + parent="declarativeCapability"> + parent="declarativeCapability"> @@ -645,8 +635,7 @@ + parent="declarativeCapability"> @@ -667,8 +656,7 @@ + parent="declarativeCapability"> @@ -687,15 +675,13 @@ + parent="declarativeCapability"> + parent="declarativeCapability"> @@ -714,8 +700,7 @@ + parent="declarativeCapability"> @@ -733,8 +718,7 @@ + parent="declarativeCapability"> @@ -746,22 +730,19 @@ + parent="declarativeCapability"> + parent="declarativeCapability"> + parent="declarativeCapability"> @@ -772,8 +753,7 @@ + parent="declarativeCapability"> @@ -784,8 +764,7 @@ + parent="declarativeCapability"> @@ -802,8 +781,7 @@ + parent="declarativeCapability"> @@ -812,41 +790,79 @@ - - - + + + - + - + - + + + + + + + + + + - + - + + + + + + + + + + + parent="compositeCapability" + class="org.alfresco.module.org_alfresco_module_rm.capability.impl.UpdatePropertiesCapability"> - + + + + + + + + + + - + - + + + + + + + + parent="declarativeCapability"> - + RECORD diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-model-context.xml b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-model-context.xml index 2b018359af..a882c2d8d1 100644 --- a/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-model-context.xml +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-model-context.xml @@ -89,6 +89,7 @@ + diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-ui-evaluators-context.xml b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-ui-evaluators-context.xml index 18d3b99aff..98d1b16067 100644 --- a/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-ui-evaluators-context.xml +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-ui-evaluators-context.xml @@ -394,6 +394,7 @@ RECORD_FOLDER + actions = new ArrayList(1); @@ -116,19 +116,19 @@ public abstract class AbstractCapability extends RMSecurityCommon } /** - * @see org.alfresco.module.org_alfresco_module_rm.capability.Capability#isGroupCapability() + * @see org.alfresco.module.org_alfresco_module_rm.capability.Capability#isPrivate() */ - public boolean isGroupCapability() + public boolean isPrivate() { - return isGroupCapability; + return isPrivate; } /** - * @param isGroupCapability indicates whether this is a group capability or not + * @param isPrivate indicates whether the capability is private or not */ - public void setGroupCapability(boolean isGroupCapability) + public void setPrivate(boolean isPrivate) { - this.isGroupCapability = isGroupCapability; + this.isPrivate = isPrivate; } /** diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/Capability.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/Capability.java index 33e1d675d0..7b0eb3c35d 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/Capability.java +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/Capability.java @@ -62,11 +62,12 @@ public interface Capability int evaluate(NodeRef source, NodeRef target); /** - * Indicates whether this is a group capability or not + * Indicates whether this is a private capability or not. Private capabilities are used internally, otherwise + * they are made available to the user to assign to roles. * * @return */ - boolean isGroupCapability(); + boolean isPrivate(); /** * Get the name of the capability diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/CapabilityService.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/CapabilityService.java index 6e5a32a88e..71b50ef918 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/CapabilityService.java +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/CapabilityService.java @@ -26,21 +26,25 @@ import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.security.AccessStatus; /** + * Capability service implementation + * * @author Roy Wetherall * @since 2.0 */ public interface CapabilityService { /** + * Register a capability * - * @param capability + * @param capability capability */ void registerCapability(Capability capability); /** + * Get a named capability. * - * @param name - * @return + * @param name capability name + * @return {@link Capability} capability or null if not found */ Capability getCapability(String name); diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/RMEntryVoter.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/RMEntryVoter.java index db7326ac98..dd8549c68c 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/RMEntryVoter.java +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/RMEntryVoter.java @@ -37,10 +37,10 @@ import net.sf.acegisecurity.vote.AccessDecisionVoter; import org.alfresco.module.org_alfresco_module_rm.RecordsManagementService; import org.alfresco.module.org_alfresco_module_rm.action.RecordsManagementAction; -import org.alfresco.module.org_alfresco_module_rm.capability.group.CreateCapability; -import org.alfresco.module.org_alfresco_module_rm.capability.group.UpdateCapability; -import org.alfresco.module.org_alfresco_module_rm.capability.group.UpdatePropertiesCapability; +import org.alfresco.module.org_alfresco_module_rm.capability.impl.CreateCapability; import org.alfresco.module.org_alfresco_module_rm.capability.impl.MoveRecordsCapability; +import org.alfresco.module.org_alfresco_module_rm.capability.impl.UpdateCapability; +import org.alfresco.module.org_alfresco_module_rm.capability.impl.UpdatePropertiesCapability; import org.alfresco.module.org_alfresco_module_rm.caveat.RMCaveatConfigComponent; import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService; import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/CompositeCapability.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/CompositeCapability.java new file mode 100644 index 0000000000..56dfcd6922 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/CompositeCapability.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2005-2012 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.module.org_alfresco_module_rm.capability.declarative; + +import java.util.List; + +import net.sf.acegisecurity.vote.AccessDecisionVoter; + +import org.alfresco.module.org_alfresco_module_rm.capability.Capability; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Generic implementation of a composite capability + * + * @author Roy Wetherall + */ +public class CompositeCapability extends DeclarativeCapability +{ + /** List of capabilities */ + private List capabilities; + + /** + * @param capabilites list of capabilities + */ + public void setCapabilities(List capabilities) + { + this.capabilities = capabilities; + } + + @Override + public int evaluateImpl(NodeRef nodeRef) + { + int result = AccessDecisionVoter.ACCESS_DENIED; + + // Check each capability using 'OR' logic + for (Capability capability : capabilities) + { + int capabilityResult = capability.evaluate(nodeRef); + if (capabilityResult == AccessDecisionVoter.ACCESS_GRANTED) + { + result = AccessDecisionVoter.ACCESS_GRANTED; + break; + } + } + + return result; + } + +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/DeclarativeCapability.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/DeclarativeCapability.java index 40e1a9ac17..ad48640e9c 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/DeclarativeCapability.java +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/DeclarativeCapability.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2011 Alfresco Software Limited. + * Copyright (C) 2005-2012 Alfresco Software Limited. * * This file is part of Alfresco * @@ -118,7 +118,7 @@ public class DeclarativeCapability extends AbstractCapability implements Applica */ protected boolean checkPermissionsImpl(NodeRef nodeRef, String ... permissions) { - boolean result = true; + boolean result = true; NodeRef filePlan = rmService.getFilePlan(nodeRef); for (String permission : permissions) diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/DeclaredCapabilityCondition.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/DeclaredCapabilityCondition.java index 76cc017214..b57801aab4 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/DeclaredCapabilityCondition.java +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/declarative/condition/DeclaredCapabilityCondition.java @@ -18,6 +18,7 @@ */ package org.alfresco.module.org_alfresco_module_rm.capability.declarative.condition; +import org.alfresco.module.org_alfresco_module_rm.FilePlanComponentKind; import org.alfresco.module.org_alfresco_module_rm.capability.declarative.AbstractCapabilityCondition; import org.alfresco.service.cmr.repository.NodeRef; @@ -29,6 +30,11 @@ public class DeclaredCapabilityCondition extends AbstractCapabilityCondition @Override public boolean evaluate(NodeRef nodeRef) { - return rmService.isRecordDeclared(nodeRef); + boolean result = false; + if (FilePlanComponentKind.RECORD.equals(rmService.getFilePlanComponentKind(nodeRef)) == true) + { + result = rmService.isRecordDeclared(nodeRef); + } + return result; } } diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/DeclareCapability.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/DeclareCapability.java deleted file mode 100644 index 060b5f83c6..0000000000 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/DeclareCapability.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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.module.org_alfresco_module_rm.capability.group; - -import net.sf.acegisecurity.vote.AccessDecisionVoter; - -import org.alfresco.module.org_alfresco_module_rm.capability.AbstractCapability; -import org.alfresco.module.org_alfresco_module_rm.capability.Capability; -import org.alfresco.module.org_alfresco_module_rm.capability.RMPermissionModel; -import org.alfresco.service.cmr.repository.NodeRef; - -/** - * Composite Declare capability - * - * @author andyh - */ -public class DeclareCapability extends AbstractCapability -{ - /* - * @see org.alfresco.module.org_alfresco_module_rm.capability.Capability#evaluate(org.alfresco.service.cmr.repository.NodeRef) - */ - public int evaluate(NodeRef declaree) - { - Capability recordsCapability = capabilityService.getCapability(RMPermissionModel.DECLARE_RECORDS); - Capability inClosedCapability = capabilityService.getCapability(RMPermissionModel.DECLARE_RECORDS_IN_CLOSED_FOLDERS); - - if (recordsCapability.hasPermissionRaw(declaree) == AccessDecisionVoter.ACCESS_GRANTED) - { - return AccessDecisionVoter.ACCESS_GRANTED; - } - if (inClosedCapability.hasPermissionRaw(declaree) == AccessDecisionVoter.ACCESS_GRANTED) - { - return AccessDecisionVoter.ACCESS_GRANTED; - } - return AccessDecisionVoter.ACCESS_DENIED; - } - -} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/DeleteCapability.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/DeleteCapability.java deleted file mode 100644 index 9b7395d851..0000000000 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/DeleteCapability.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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.module.org_alfresco_module_rm.capability.group; - -import net.sf.acegisecurity.vote.AccessDecisionVoter; - -import org.alfresco.module.org_alfresco_module_rm.capability.AbstractCapability; -import org.alfresco.module.org_alfresco_module_rm.capability.Capability; -import org.alfresco.module.org_alfresco_module_rm.capability.RMPermissionModel; -import org.alfresco.service.cmr.repository.NodeRef; - -/** - * @author andyh - */ -public class DeleteCapability extends AbstractCapability -{ - /** - * @see org.alfresco.module.org_alfresco_module_rm.capability.Capability#evaluate(org.alfresco.service.cmr.repository.NodeRef) - */ - public int evaluate(NodeRef deletee) - { - Capability schedRec = capabilityService.getCapability(RMPermissionModel.DESTROY_RECORDS_SCHEDULED_FOR_DESTRUCTION); - Capability destroy = capabilityService.getCapability(RMPermissionModel.DESTROY_RECORDS); - Capability delete = capabilityService.getCapability(RMPermissionModel.DELETE_RECORDS); - Capability desfileplan = capabilityService.getCapability(RMPermissionModel.CREATE_MODIFY_DESTROY_FILEPLAN_METADATA); - Capability desfolder = capabilityService.getCapability(RMPermissionModel.CREATE_MODIFY_DESTROY_FOLDERS); - - if (schedRec.evaluate(deletee) == AccessDecisionVoter.ACCESS_GRANTED) - { - return AccessDecisionVoter.ACCESS_GRANTED; - } - if (destroy.evaluate(deletee) == AccessDecisionVoter.ACCESS_GRANTED) - { - return AccessDecisionVoter.ACCESS_GRANTED; - } - if (delete.evaluate(deletee) == AccessDecisionVoter.ACCESS_GRANTED) - { - return AccessDecisionVoter.ACCESS_GRANTED; - } - if (desfileplan.evaluate(deletee) == AccessDecisionVoter.ACCESS_GRANTED) - { - return AccessDecisionVoter.ACCESS_GRANTED; - } - if (desfolder.evaluate(deletee, null) == AccessDecisionVoter.ACCESS_GRANTED) - { - return AccessDecisionVoter.ACCESS_GRANTED; - } - return AccessDecisionVoter.ACCESS_DENIED; - } - -} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/UpdateCapability.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/UpdateCapability.java deleted file mode 100644 index 671dad38a9..0000000000 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/UpdateCapability.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * 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.module.org_alfresco_module_rm.capability.group; - -import java.io.Serializable; -import java.util.Map; - -import net.sf.acegisecurity.vote.AccessDecisionVoter; - -import org.alfresco.module.org_alfresco_module_rm.capability.AbstractCapability; -import org.alfresco.module.org_alfresco_module_rm.capability.Capability; -import org.alfresco.service.cmr.repository.NodeRef; -import org.alfresco.service.namespace.QName; - -/** - * @author andyh - */ -public class UpdateCapability extends AbstractCapability -{ - /** - * @see org.alfresco.module.org_alfresco_module_rm.capability.Capability#evaluate(org.alfresco.service.cmr.repository.NodeRef) - */ - @Override - public int evaluate(NodeRef nodeRef) - { - return evaluate(nodeRef, null, null); - } - - /** - * - * @param nodeRef - * @param aspectQName - * @param properties - * @return - */ - public int evaluate(NodeRef nodeRef, QName aspectQName, Map properties) - { - if ((aspectQName != null) && (voter.isProtectedAspect(nodeRef, aspectQName))) - { - return AccessDecisionVoter.ACCESS_DENIED; - } - if ((properties != null) && (voter.includesProtectedPropertyChange(nodeRef, properties))) - { - return AccessDecisionVoter.ACCESS_DENIED; - } - - Capability destFolder = capabilityService.getCapability(CREATE_MODIFY_DESTROY_FOLDERS); - if (destFolder.evaluate(nodeRef) == AccessDecisionVoter.ACCESS_GRANTED) - { - return AccessDecisionVoter.ACCESS_GRANTED; - } - - Capability fileplanMeta = capabilityService.getCapability(CREATE_MODIFY_DESTROY_FILEPLAN_METADATA); - if (fileplanMeta.evaluate(nodeRef) == AccessDecisionVoter.ACCESS_GRANTED) - { - return AccessDecisionVoter.ACCESS_GRANTED; - } - - Capability recordMeta = capabilityService.getCapability(EDIT_DECLARED_RECORD_METADATA); - if (recordMeta.evaluate(nodeRef) == AccessDecisionVoter.ACCESS_GRANTED) - { - return AccessDecisionVoter.ACCESS_GRANTED; - } - - Capability nonRecordMetadata = capabilityService.getCapability(EDIT_NON_RECORD_METADATA); - if (nonRecordMetadata.evaluate(nodeRef) == AccessDecisionVoter.ACCESS_GRANTED) - { - return AccessDecisionVoter.ACCESS_GRANTED; - } - - Capability editRecordMetadata = capabilityService.getCapability(EDIT_RECORD_METADATA); - if (editRecordMetadata.evaluate(nodeRef) == AccessDecisionVoter.ACCESS_GRANTED) - { - return AccessDecisionVoter.ACCESS_GRANTED; - } - - return AccessDecisionVoter.ACCESS_DENIED; - } -} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/UpdatePropertiesCapability.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/UpdatePropertiesCapability.java deleted file mode 100644 index b1423fcd29..0000000000 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/UpdatePropertiesCapability.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * 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.module.org_alfresco_module_rm.capability.group; - -import java.io.Serializable; -import java.util.Map; - -import net.sf.acegisecurity.vote.AccessDecisionVoter; - -import org.alfresco.module.org_alfresco_module_rm.capability.AbstractCapability; -import org.alfresco.module.org_alfresco_module_rm.capability.Capability; -import org.alfresco.service.cmr.repository.NodeRef; -import org.alfresco.service.namespace.QName; - -/** - * @author andyh - */ -public class UpdatePropertiesCapability extends AbstractCapability -{ - /** - * @see org.alfresco.module.org_alfresco_module_rm.capability.Capability#evaluate(org.alfresco.service.cmr.repository.NodeRef) - */ - @Override - public int evaluate(NodeRef nodeRef) - { - return evaluate(nodeRef, (Map)null); - } - - /** - * Evaluate cabability - * - * @param nodeRef - * @param properties - * @return - */ - public int evaluate(NodeRef nodeRef, Map properties) - { - if ((properties != null) && (voter.includesProtectedPropertyChange(nodeRef, properties))) - { - return AccessDecisionVoter.ACCESS_DENIED; - } - - Capability cap1 = capabilityService.getCapability(CREATE_MODIFY_DESTROY_FOLDERS); - if (cap1.evaluate(nodeRef) == AccessDecisionVoter.ACCESS_GRANTED) - { - return AccessDecisionVoter.ACCESS_GRANTED; - } - - Capability cap2 = capabilityService.getCapability(CREATE_MODIFY_DESTROY_FILEPLAN_METADATA); - if (cap2.evaluate(nodeRef) == AccessDecisionVoter.ACCESS_GRANTED) - { - return AccessDecisionVoter.ACCESS_GRANTED; - } - - Capability cap3 = capabilityService.getCapability(EDIT_DECLARED_RECORD_METADATA); - if (cap3.evaluate(nodeRef) == AccessDecisionVoter.ACCESS_GRANTED) - { - return AccessDecisionVoter.ACCESS_GRANTED; - } - - Capability cap4 = capabilityService.getCapability(EDIT_NON_RECORD_METADATA); - if (cap4.evaluate(nodeRef) == AccessDecisionVoter.ACCESS_GRANTED) - { - return AccessDecisionVoter.ACCESS_GRANTED; - } - - Capability cap5 = capabilityService.getCapability(EDIT_RECORD_METADATA); - if (cap5.evaluate(nodeRef) == AccessDecisionVoter.ACCESS_GRANTED) - { - return AccessDecisionVoter.ACCESS_GRANTED; - } - - Capability cap6 = capabilityService.getCapability(CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS); - if (cap6.evaluate(nodeRef) == AccessDecisionVoter.ACCESS_GRANTED) - { - return AccessDecisionVoter.ACCESS_GRANTED; - } - - return AccessDecisionVoter.ACCESS_DENIED; - } -} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/WriteContentCapability.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/WriteContentCapability.java deleted file mode 100644 index 0da5b4a3fc..0000000000 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/WriteContentCapability.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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.module.org_alfresco_module_rm.capability.group; - -import net.sf.acegisecurity.vote.AccessDecisionVoter; - -import org.alfresco.module.org_alfresco_module_rm.capability.RMPermissionModel; -import org.alfresco.module.org_alfresco_module_rm.capability.declarative.DeclarativeCapability; -import org.alfresco.service.cmr.repository.NodeRef; -import org.alfresco.service.cmr.security.AccessStatus; - -/** - * @author andyh - */ -public class WriteContentCapability extends DeclarativeCapability -{ - /** - * @see org.alfresco.module.org_alfresco_module_rm.capability.Capability#evaluate(org.alfresco.service.cmr.repository.NodeRef) - */ - public int evaluate(NodeRef nodeRef) - { - int result = AccessDecisionVoter.ACCESS_ABSTAIN; - - if (rmService.isFilePlanComponent(nodeRef)) - { - result = AccessDecisionVoter.ACCESS_DENIED; - - if (checkKinds(nodeRef) == true && checkConditions(nodeRef) == true) - { - if (permissionService.hasPermission(nodeRef, RMPermissionModel.FILE_RECORDS) == AccessStatus.ALLOWED) - { - result = AccessDecisionVoter.ACCESS_GRANTED; - } - } - } - - return result; - } -} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/CreateCapability.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/CreateCapability.java similarity index 95% rename from rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/CreateCapability.java rename to rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/CreateCapability.java index b19d558a4e..a54bda2968 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/group/CreateCapability.java +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/CreateCapability.java @@ -16,7 +16,7 @@ * You should have received a copy of the GNU Lesser General Public License * along with Alfresco. If not, see . */ -package org.alfresco.module.org_alfresco_module_rm.capability.group; +package org.alfresco.module.org_alfresco_module_rm.capability.impl; import java.util.HashMap; import java.util.Map; @@ -26,7 +26,6 @@ import net.sf.acegisecurity.vote.AccessDecisionVoter; import org.alfresco.model.ContentModel; import org.alfresco.module.org_alfresco_module_rm.capability.RMPermissionModel; import org.alfresco.module.org_alfresco_module_rm.capability.declarative.DeclarativeCapability; -import org.alfresco.module.org_alfresco_module_rm.capability.impl.ChangeOrDeleteReferencesCapability; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.security.AccessStatus; import org.alfresco.service.namespace.QName; diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/FileRecordsCapability.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/FileRecordsCapability.java deleted file mode 100644 index e8ecaa6829..0000000000 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/FileRecordsCapability.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * 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.module.org_alfresco_module_rm.capability.impl; - -import java.util.HashMap; -import java.util.Map; - -import net.sf.acegisecurity.vote.AccessDecisionVoter; - -import org.alfresco.model.ContentModel; -import org.alfresco.module.org_alfresco_module_rm.capability.RMPermissionModel; -import org.alfresco.module.org_alfresco_module_rm.capability.declarative.DeclarativeCapability; -import org.alfresco.service.cmr.dictionary.DictionaryService; -import org.alfresco.service.cmr.repository.NodeRef; -import org.alfresco.service.cmr.security.AccessStatus; -import org.alfresco.service.namespace.QName; - -/** - * File records capability. - * - * @author andyh - */ -public class FileRecordsCapability extends DeclarativeCapability -{ - /** Dictionary service */ - private DictionaryService dictionaryService; - - /** - * @param dictionaryService dictionary service - */ - public void setDictionaryService(DictionaryService dictionaryService) - { - this.dictionaryService = dictionaryService; - } - - /** - * @see org.alfresco.module.org_alfresco_module_rm.capability.declarative.DeclarativeCapability#evaluate(org.alfresco.service.cmr.repository.NodeRef) - */ - public int evaluate(NodeRef nodeRef) - { - if (rmService.isFilePlanComponent(nodeRef)) - { - // Build the conditions map - Map conditions = new HashMap(5); - conditions.put("capabilityCondition.filling", Boolean.TRUE); - conditions.put("capabilityCondition.frozen", Boolean.FALSE); - conditions.put("capabilityCondition.cutoff", Boolean.FALSE); - conditions.put("capabilityCondition.closed", Boolean.FALSE); - conditions.put("capabilityCondition.declared", Boolean.FALSE); - - if (isFileable(nodeRef) || (rmService.isRecord(nodeRef) && checkConditions(nodeRef, conditions) == true)) - { - if (permissionService.hasPermission(nodeRef, RMPermissionModel.FILE_RECORDS) == AccessStatus.ALLOWED) - { - return AccessDecisionVoter.ACCESS_GRANTED; - } - } - - conditions.put("capabilityCondition.closed", Boolean.TRUE); - if (isFileable(nodeRef) || (rmService.isRecord(nodeRef) && checkConditions(nodeRef, conditions) == true)) - { - if (checkPermissionsImpl(nodeRef, DECLARE_RECORDS_IN_CLOSED_FOLDERS) == true) - { - return AccessDecisionVoter.ACCESS_GRANTED; - } - } - - conditions.put("capabilityCondition.cutoff", Boolean.TRUE); - conditions.remove("capabilityCondition.closed"); - conditions.remove("capabilityCondition.declared"); - if (isFileable(nodeRef) || (rmService.isRecord(nodeRef) && checkConditions(nodeRef, conditions) == true)) - { - if (checkPermissionsImpl(nodeRef, CREATE_MODIFY_RECORDS_IN_CUTOFF_FOLDERS) == true) - { - return AccessDecisionVoter.ACCESS_GRANTED; - } - } - - return AccessDecisionVoter.ACCESS_DENIED; - - } - else - { - return AccessDecisionVoter.ACCESS_ABSTAIN; - } - } - - /** - * Indicate whether a node if 'fileable' or not. - * - * @param nodeRef node reference - * @return boolean true if the node is filable, false otherwise - */ - public boolean isFileable(NodeRef nodeRef) - { - QName type = nodeService.getType(nodeRef); - return dictionaryService.isSubClass(type, ContentModel.TYPE_CONTENT); - } -} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/MoveRecordsCapability.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/MoveRecordsCapability.java index 376172dcfc..4b25b7f271 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/MoveRecordsCapability.java +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/MoveRecordsCapability.java @@ -21,7 +21,6 @@ package org.alfresco.module.org_alfresco_module_rm.capability.impl; import net.sf.acegisecurity.vote.AccessDecisionVoter; import org.alfresco.module.org_alfresco_module_rm.capability.declarative.DeclarativeCapability; -import org.alfresco.module.org_alfresco_module_rm.capability.group.CreateCapability; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.security.PermissionService; import org.alfresco.service.namespace.QName; diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/UpdateCapability.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/UpdateCapability.java new file mode 100644 index 0000000000..db528dcd0c --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/UpdateCapability.java @@ -0,0 +1,58 @@ +/* + * 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.module.org_alfresco_module_rm.capability.impl; + +import java.io.Serializable; +import java.util.Map; + +import net.sf.acegisecurity.vote.AccessDecisionVoter; + +import org.alfresco.module.org_alfresco_module_rm.capability.declarative.CompositeCapability; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + +/** + * Update capability implementation. + * + * @author andyh + */ +public class UpdateCapability extends CompositeCapability +{ + /** + * + * @param nodeRef + * @param aspectQName + * @param properties + * @return + */ + public int evaluate(NodeRef nodeRef, QName aspectQName, Map properties) + { + if ((aspectQName != null) && (voter.isProtectedAspect(nodeRef, aspectQName))) + { + return AccessDecisionVoter.ACCESS_DENIED; + } + + if ((properties != null) && (voter.includesProtectedPropertyChange(nodeRef, properties))) + { + return AccessDecisionVoter.ACCESS_DENIED; + } + + return evaluate(nodeRef); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/UpdatePropertiesCapability.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/UpdatePropertiesCapability.java new file mode 100644 index 0000000000..130b12849a --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/capability/impl/UpdatePropertiesCapability.java @@ -0,0 +1,52 @@ +/* + * 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.module.org_alfresco_module_rm.capability.impl; + +import java.io.Serializable; +import java.util.Map; + +import net.sf.acegisecurity.vote.AccessDecisionVoter; + +import org.alfresco.module.org_alfresco_module_rm.capability.declarative.CompositeCapability; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; + +/** + * Update properties capability + * + * @author andyh + */ +public class UpdatePropertiesCapability extends CompositeCapability +{ + /** + * Evaluate capability, taking into account the protected properties. + * + * @param nodeRef node reference + * @param properties updated properties, if no null + */ + public int evaluate(NodeRef nodeRef, Map properties) + { + if ((properties != null) && (voter.includesProtectedPropertyChange(nodeRef, properties))) + { + return AccessDecisionVoter.ACCESS_DENIED; + } + + return evaluate(nodeRef); + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordContainerType.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordContainerType.java index 747a7647ce..5bd85b7f1e 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordContainerType.java +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordContainerType.java @@ -186,6 +186,6 @@ public class RecordContainerType implements RecordsManagementModel, } return null; } - }, AuthenticationUtil.getAdminUserName()); + }, AuthenticationUtil.getSystemUserName()); } } diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/security/RecordsManagementSecurityServiceImpl.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/security/RecordsManagementSecurityServiceImpl.java index b9284eecd8..513bac8102 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/security/RecordsManagementSecurityServiceImpl.java +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/security/RecordsManagementSecurityServiceImpl.java @@ -354,7 +354,7 @@ public class RecordsManagementSecurityServiceImpl implements RecordsManagementSe Set result = new HashSet(caps.size()); for (Capability cap : caps) { - if (cap.isGroupCapability() == false) + if (cap.isPrivate() == false) { result.add(cap); } diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/vital/VitalRecordServiceImpl.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/vital/VitalRecordServiceImpl.java index 7ab4305c76..ef8935bb87 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/vital/VitalRecordServiceImpl.java +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/vital/VitalRecordServiceImpl.java @@ -29,6 +29,8 @@ import org.alfresco.repo.node.NodeServicePolicies; import org.alfresco.repo.policy.JavaBehaviour; import org.alfresco.repo.policy.PolicyComponent; import org.alfresco.repo.policy.Behaviour.NotificationFrequency; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.repository.Period; @@ -122,7 +124,7 @@ public class VitalRecordServiceImpl implements VitalRecordService, * @see org.alfresco.repo.node.NodeServicePolicies.OnAddAspectPolicy#onAddAspect(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName) */ @Override - public void onAddAspect(NodeRef nodeRef, QName aspectTypeQName) + public void onAddAspect(final NodeRef nodeRef, final QName aspectTypeQName) { ParameterCheck.mandatory("nodeRef", nodeRef); ParameterCheck.mandatory("aspectTypeQName", aspectTypeQName); @@ -132,27 +134,35 @@ public class VitalRecordServiceImpl implements VitalRecordService, onUpdateProperties.disable(); try { - // get the immediate parent - NodeRef parentRef = nodeService.getPrimaryParent(nodeRef).getParentRef(); - - // is the parent a record category - if (parentRef != null && - FilePlanComponentKind.RECORD_CATEGORY.equals(rmService.getFilePlanComponentKind(parentRef)) == true) + AuthenticationUtil.runAs(new RunAsWork() { - // is the child a record category or folder - FilePlanComponentKind kind = rmService.getFilePlanComponentKind(nodeRef); - if (kind.equals(FilePlanComponentKind.RECORD_CATEGORY) == true || - kind.equals(FilePlanComponentKind.RECORD_FOLDER) == true) + public Void doWork() throws Exception { - // set the vital record definition values to match that of the parent - nodeService.setProperty(nodeRef, - PROP_VITAL_RECORD_INDICATOR, - nodeService.getProperty(parentRef, PROP_VITAL_RECORD_INDICATOR)); - nodeService.setProperty(nodeRef, - PROP_REVIEW_PERIOD, - nodeService.getProperty(parentRef, PROP_REVIEW_PERIOD)); + // get the immediate parent + NodeRef parentRef = nodeService.getPrimaryParent(nodeRef).getParentRef(); + + // is the parent a record category + if (parentRef != null && + FilePlanComponentKind.RECORD_CATEGORY.equals(rmService.getFilePlanComponentKind(parentRef)) == true) + { + // is the child a record category or folder + FilePlanComponentKind kind = rmService.getFilePlanComponentKind(nodeRef); + if (kind.equals(FilePlanComponentKind.RECORD_CATEGORY) == true || + kind.equals(FilePlanComponentKind.RECORD_FOLDER) == true) + { + // set the vital record definition values to match that of the parent + nodeService.setProperty(nodeRef, + PROP_VITAL_RECORD_INDICATOR, + nodeService.getProperty(parentRef, PROP_VITAL_RECORD_INDICATOR)); + nodeService.setProperty(nodeRef, + PROP_REVIEW_PERIOD, + nodeService.getProperty(parentRef, PROP_REVIEW_PERIOD)); + } + } + + return null; } - } + }, AuthenticationUtil.getSystemUserName()); } finally { diff --git a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/CapabilitiesTestSuite.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/CapabilitiesTestSuite.java index 9be78c443d..a21edd0d7c 100644 --- a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/CapabilitiesTestSuite.java +++ b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/CapabilitiesTestSuite.java @@ -23,6 +23,7 @@ import junit.framework.TestSuite; import org.alfresco.module.org_alfresco_module_rm.test.capabilities.CapabilitiesTest; import org.alfresco.module.org_alfresco_module_rm.test.capabilities.DeclarativeCapabilityTest; +import org.alfresco.module.org_alfresco_module_rm.test.capabilities.CompositeCapabilityTest; /** @@ -42,6 +43,7 @@ public class CapabilitiesTestSuite extends TestSuite TestSuite suite = new TestSuite(); suite.addTestSuite(CapabilitiesTest.class); suite.addTestSuite(DeclarativeCapabilityTest.class); + suite.addTestSuite(CompositeCapabilityTest.class); return suite; } } diff --git a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/CapabilitiesTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/CapabilitiesTest.java index 3e7419f79e..5ae06de2a9 100644 --- a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/CapabilitiesTest.java +++ b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/CapabilitiesTest.java @@ -82,6 +82,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements // Give all the users file permission objects for (String user : testUsers) { + securityService.setPermission(filePlan, user, FILING); securityService.setPermission(rmContainer, user, FILING); } } @@ -424,7 +425,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements Map access = securityService .getCapabilities(filePlan); - assertEquals(65, access.size()); + assertEquals(66, access.size()); check(access, ACCESS_AUDIT, AccessStatus.ALLOWED); check(access, ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); @@ -544,7 +545,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements .getAdminUserName()); Map access = securityService .getCapabilities(filePlan); - assertEquals(65, access.size()); + assertEquals(66, access.size()); check(access, ACCESS_AUDIT, AccessStatus.ALLOWED); check(access, ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); @@ -666,7 +667,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements .setFullyAuthenticatedUser(rmAdminName); Map access = securityService .getCapabilities(filePlan); - assertEquals(65, access.size()); + assertEquals(66, access.size()); check(access, ACCESS_AUDIT, AccessStatus.ALLOWED); check(access, ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); @@ -794,7 +795,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements .setFullyAuthenticatedUser(recordsManagerName); Map access = securityService .getCapabilities(filePlan); - assertEquals(65, access.size()); // 58 + File + assertEquals(66, access.size()); // 58 + File check(access, ACCESS_AUDIT, AccessStatus.ALLOWED); check(access, ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); @@ -807,7 +808,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements check(access, AUTHORIZE_NOMINATED_TRANSFERS, AccessStatus.DENIED); check(access, CHANGE_OR_DELETE_REFERENCES, - AccessStatus.UNDETERMINED); + AccessStatus.DENIED); check(access, CLOSE_FOLDERS, AccessStatus.DENIED); check(access, CREATE_AND_ASSOCIATE_SELECTION_LISTS, AccessStatus.ALLOWED); @@ -817,7 +818,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements check(access, CREATE_MODIFY_DESTROY_EVENTS, AccessStatus.ALLOWED); check(access, CREATE_MODIFY_DESTROY_FILEPLAN_METADATA, - AccessStatus.ALLOWED); + AccessStatus.DENIED); check(access, CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.ALLOWED); check(access, CREATE_MODIFY_DESTROY_FOLDERS, @@ -918,7 +919,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements .setFullyAuthenticatedUser(securityOfficerName); Map access = securityService .getCapabilities(filePlan); - assertEquals(65, access.size()); // 58 + File + assertEquals(66, access.size()); // 58 + File check(access, ACCESS_AUDIT, AccessStatus.DENIED); check(access, ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); @@ -1037,7 +1038,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements .setFullyAuthenticatedUser(powerUserName); Map access = securityService .getCapabilities(filePlan); - assertEquals(65, access.size()); // 58 + File + assertEquals(66, access.size()); // 58 + File check(access, ACCESS_AUDIT, AccessStatus.DENIED); check(access, ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); @@ -1156,7 +1157,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements .setFullyAuthenticatedUser(rmUserName); Map access = securityService .getCapabilities(filePlan); - assertEquals(65, access.size()); // 58 + File + assertEquals(66, access.size()); // 58 + File check(access, ACCESS_AUDIT, AccessStatus.DENIED); check(access, ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); @@ -1276,7 +1277,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements Map access = securityService .getCapabilities(rmContainer); - assertEquals(65, access.size()); + assertEquals(66, access.size()); check(access, ACCESS_AUDIT, AccessStatus.ALLOWED); check(access, ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); @@ -1399,7 +1400,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements .getAdminUserName()); Map access = securityService .getCapabilities(rmContainer); - assertEquals(65, access.size()); + assertEquals(66, access.size()); check(access, ACCESS_AUDIT, AccessStatus.ALLOWED); check(access, ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); @@ -1521,7 +1522,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements .setFullyAuthenticatedUser(rmAdminName); Map access = securityService .getCapabilities(rmContainer); - assertEquals(65, access.size()); + assertEquals(66, access.size()); check(access, ACCESS_AUDIT, AccessStatus.ALLOWED); check(access, ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); @@ -1645,7 +1646,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements // rm_records_manager, FILING, true); Map access = securityService .getCapabilities(rmContainer); - assertEquals(65, access.size()); // 58 + File + assertEquals(66, access.size()); // 58 + File check(access, ACCESS_AUDIT, AccessStatus.ALLOWED); check(access, ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); @@ -1769,7 +1770,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements // securityOfficerName, FILING, true); Map access = securityService .getCapabilities(rmContainer); - assertEquals(65, access.size()); // 58 + File + assertEquals(66, access.size()); // 58 + File check(access, ACCESS_AUDIT, AccessStatus.DENIED); check(access, ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); @@ -1890,7 +1891,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements // powerUserName, FILING, true); Map access = securityService .getCapabilities(rmContainer); - assertEquals(65, access.size()); // 58 + File + assertEquals(66, access.size()); // 58 + File check(access, ACCESS_AUDIT, AccessStatus.DENIED); check(access, ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); @@ -2011,7 +2012,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements // rmUserName, FILING, true); Map access = securityService .getCapabilities(rmContainer); - assertEquals(65, access.size()); // 58 + File + assertEquals(66, access.size()); // 58 + File check(access, ACCESS_AUDIT, AccessStatus.DENIED); check(access, ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); @@ -2131,7 +2132,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements Map access = securityService .getCapabilities(rmFolder); - assertEquals(65, access.size()); + assertEquals(66, access.size()); check(access, ACCESS_AUDIT, AccessStatus.ALLOWED); check(access, ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); @@ -2260,7 +2261,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements .getAdminUserName()); Map access = securityService .getCapabilities(rmFolder); - assertEquals(65, access.size()); + assertEquals(66, access.size()); check(access, ACCESS_AUDIT, AccessStatus.ALLOWED); check(access, ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); @@ -2383,7 +2384,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements .setFullyAuthenticatedUser(rmAdminName); Map access = securityService .getCapabilities(rmFolder); - assertEquals(65, access.size()); + assertEquals(66, access.size()); check(access, ACCESS_AUDIT, AccessStatus.ALLOWED); check(access, ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); @@ -2504,7 +2505,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements AuthenticationUtil.setFullyAuthenticatedUser(recordsManagerName); //setFilingOnRecordFolder(rmFolder, recordsManagerName); Map access = securityService.getCapabilities(rmFolder); - assertEquals(65, access.size()); // 58 + File + assertEquals(66, access.size()); // 58 + File check(access, ACCESS_AUDIT, AccessStatus.ALLOWED); check(access, ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); @@ -2625,7 +2626,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements AuthenticationUtil.setFullyAuthenticatedUser(securityOfficerName); //setFilingOnRecordFolder(rmFolder, securityOfficerName); Map access = securityService.getCapabilities(rmFolder); - assertEquals(65, access.size()); // 58 + File + assertEquals(66, access.size()); // 58 + File check(access, ACCESS_AUDIT, AccessStatus.DENIED); check(access, ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); @@ -2743,7 +2744,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements AuthenticationUtil.setFullyAuthenticatedUser(powerUserName); //setFilingOnRecordFolder(rmFolder, powerUserName); Map access = securityService.getCapabilities(rmFolder); - assertEquals(65, access.size()); // 58 + File + assertEquals(66, access.size()); // 58 + File check(access, ACCESS_AUDIT, AccessStatus.DENIED); check(access, ADD_MODIFY_EVENT_DATES, AccessStatus.ALLOWED); @@ -2863,7 +2864,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements //setFilingOnRecordFolder(rmFolder, rmUserName); Map access = securityService .getCapabilities(rmFolder); - assertEquals(65, access.size()); // 58 + File + assertEquals(66, access.size()); // 58 + File check(access, ACCESS_AUDIT, AccessStatus.DENIED); check(access, ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); @@ -2980,7 +2981,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements { AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.SYSTEM_USER_NAME); Map access = securityService.getCapabilities(record); - assertEquals(65, access.size()); + assertEquals(66, access.size()); check(access, ACCESS_AUDIT, AccessStatus.ALLOWED); check(access, ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); @@ -3007,7 +3008,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements check(access, CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.ALLOWED); check(access, CREATE_MODIFY_DESTROY_FOLDERS, - AccessStatus.ALLOWED); + AccessStatus.DENIED); check(access, CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.ALLOWED); check(access, CREATE_MODIFY_DESTROY_REFERENCE_TYPES, @@ -3104,7 +3105,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements .getAdminUserName()); Map access = securityService .getCapabilities(record); - assertEquals(65, access.size()); + assertEquals(66, access.size()); check(access, ACCESS_AUDIT, AccessStatus.ALLOWED); check(access, ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); @@ -3131,7 +3132,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements check(access, CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.ALLOWED); check(access, CREATE_MODIFY_DESTROY_FOLDERS, - AccessStatus.ALLOWED); + AccessStatus.DENIED); check(access, CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.ALLOWED); check(access, CREATE_MODIFY_DESTROY_REFERENCE_TYPES, @@ -3227,7 +3228,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements .setFullyAuthenticatedUser(rmAdminName); Map access = securityService .getCapabilities(record); - assertEquals(65, access.size()); + assertEquals(66, access.size()); check(access, ACCESS_AUDIT, AccessStatus.ALLOWED); check(access, ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); @@ -3254,7 +3255,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements check(access, CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.ALLOWED); check(access, CREATE_MODIFY_DESTROY_FOLDERS, - AccessStatus.ALLOWED); + AccessStatus.DENIED); check(access, CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.ALLOWED); check(access, CREATE_MODIFY_DESTROY_REFERENCE_TYPES, @@ -3351,7 +3352,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements // setFilingOnRecord(record, recordsManagerName); Map access = securityService .getCapabilities(record); - assertEquals(65, access.size()); // 58 + File + assertEquals(66, access.size()); // 58 + File check(access, ACCESS_AUDIT, AccessStatus.ALLOWED); check(access, ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); @@ -3378,7 +3379,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements check(access, CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.ALLOWED); check(access, CREATE_MODIFY_DESTROY_FOLDERS, - AccessStatus.ALLOWED); + AccessStatus.DENIED); check(access, CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.ALLOWED); check(access, CREATE_MODIFY_DESTROY_REFERENCE_TYPES, @@ -3475,7 +3476,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements // setFilingOnRecord(record, securityOfficerName); Map access = securityService .getCapabilities(record); - assertEquals(65, access.size()); // 58 + File + assertEquals(66, access.size()); // 58 + File check(access, ACCESS_AUDIT, AccessStatus.DENIED); check(access, ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); @@ -3502,7 +3503,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements check(access, CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.DENIED); check(access, CREATE_MODIFY_DESTROY_FOLDERS, - AccessStatus.ALLOWED); + AccessStatus.DENIED); check(access, CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.DENIED); check(access, CREATE_MODIFY_DESTROY_REFERENCE_TYPES, @@ -3597,7 +3598,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements // setFilingOnRecord(record, powerUserName); Map access = securityService .getCapabilities(record); - assertEquals(65, access.size()); // 58 + File + assertEquals(66, access.size()); // 58 + File check(access, ACCESS_AUDIT, AccessStatus.DENIED); check(access, ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); @@ -3624,7 +3625,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements check(access, CREATE_MODIFY_DESTROY_FILEPLAN_TYPES, AccessStatus.DENIED); check(access, CREATE_MODIFY_DESTROY_FOLDERS, - AccessStatus.ALLOWED); + AccessStatus.DENIED); check(access, CREATE_MODIFY_DESTROY_RECORD_TYPES, AccessStatus.DENIED); check(access, CREATE_MODIFY_DESTROY_REFERENCE_TYPES, @@ -3718,7 +3719,7 @@ public class CapabilitiesTest extends BaseRMTestCase implements // setFilingOnRecord(record, rmUserName); Map access = securityService .getCapabilities(record); - assertEquals(65, access.size()); // 58 + File + assertEquals(66, access.size()); // 58 + File check(access, ACCESS_AUDIT, AccessStatus.DENIED); check(access, ADD_MODIFY_EVENT_DATES, AccessStatus.DENIED); diff --git a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/GroupCapabilityTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/CompositeCapabilityTest.java similarity index 96% rename from rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/GroupCapabilityTest.java rename to rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/CompositeCapabilityTest.java index 10ac2abccd..cbdbe5439c 100644 --- a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/GroupCapabilityTest.java +++ b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/CompositeCapabilityTest.java @@ -31,7 +31,7 @@ import org.alfresco.service.cmr.security.AccessStatus; * * @author Roy Wetherall */ -public class GroupCapabilityTest extends BaseRMTestCase +public class CompositeCapabilityTest extends BaseRMTestCase { private NodeRef record; private NodeRef declaredRecord; diff --git a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/DeclarativeCapabilityTest.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/DeclarativeCapabilityTest.java index f20582d61a..f764510b43 100644 --- a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/DeclarativeCapabilityTest.java +++ b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/capabilities/DeclarativeCapabilityTest.java @@ -51,6 +51,8 @@ public class DeclarativeCapabilityTest extends BaseRMTestCase private NodeRef frozenRecord2; private NodeRef frozenRecordFolder; + private NodeRef closedFolder; + @Override protected boolean isUserTest() { @@ -66,9 +68,9 @@ public class DeclarativeCapabilityTest extends BaseRMTestCase record = utils.createRecord(rmFolder, "record.txt"); declaredRecord = utils.createRecord(rmFolder, "declaredRecord.txt"); - - // Open folder // Closed folder + closedFolder = rmService.createRecordFolder(rmContainer, "closedFolder"); + utils.closeFolder(closedFolder); recordFolderContainsFrozen = rmService.createRecordFolder(rmContainer, "containsFrozen"); frozenRecord = utils.createRecord(rmFolder, "frozenRecord.txt"); @@ -130,7 +132,7 @@ public class DeclarativeCapabilityTest extends BaseRMTestCase for (Capability capability : capabilities) { if (capability instanceof DeclarativeCapability && - capability.isGroupCapability() == false && + capability.isPrivate() == false && capability.getName().equals("MoveRecords") == false && capability.getName().equals("DeleteLinks") == false && capability.getName().equals("ChangeOrDeleteReferences") == false && @@ -237,4 +239,48 @@ public class DeclarativeCapabilityTest extends BaseRMTestCase return result; } + + /** Specific declarative capability tests */ + + public void testFileCapability() + { + final Capability capability = capabilityService.getCapability("File"); + assertNotNull(capability); + + doTestInTransaction(new Test() + { + @Override + public Void run() + { + assertEquals(AccessStatus.DENIED, capability.hasPermission(rmContainer)); + assertEquals(AccessStatus.ALLOWED, capability.hasPermission(rmFolder)); + assertEquals(AccessStatus.ALLOWED, capability.hasPermission(record)); + assertEquals(AccessStatus.DENIED, capability.hasPermission(declaredRecord)); + assertEquals(AccessStatus.DENIED, capability.hasPermission(frozenRecordFolder)); + assertEquals(AccessStatus.DENIED, capability.hasPermission(recordFolderContainsFrozen)); + assertEquals(AccessStatus.DENIED, capability.hasPermission(frozenRecord)); + assertEquals(AccessStatus.DENIED, capability.hasPermission(closedFolder)); + + return null; + } + }, recordsManagerName); + + doTestInTransaction(new Test() + { + @Override + public Void run() + { + assertEquals(AccessStatus.DENIED, capability.hasPermission(rmContainer)); + assertEquals(AccessStatus.ALLOWED, capability.hasPermission(rmFolder)); + assertEquals(AccessStatus.ALLOWED, capability.hasPermission(record)); + assertEquals(AccessStatus.DENIED, capability.hasPermission(declaredRecord)); + assertEquals(AccessStatus.DENIED, capability.hasPermission(frozenRecordFolder)); + assertEquals(AccessStatus.DENIED, capability.hasPermission(recordFolderContainsFrozen)); + assertEquals(AccessStatus.DENIED, capability.hasPermission(frozenRecord)); + assertEquals(AccessStatus.DENIED, capability.hasPermission(closedFolder)); + + return null; + } + }, rmUserName); + } } diff --git a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/util/CommonRMTestUtils.java b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/util/CommonRMTestUtils.java index e146b08575..202fed6b36 100644 --- a/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/util/CommonRMTestUtils.java +++ b/rm-server/test/java/org/alfresco/module/org_alfresco_module_rm/test/util/CommonRMTestUtils.java @@ -169,6 +169,19 @@ public class CommonRMTestUtils implements RecordsManagementModel } + public void closeFolder(final NodeRef recordFolder) + { + AuthenticationUtil.runAs(new RunAsWork() + { + @Override + public Void doWork() throws Exception + { + actionService.executeRecordsManagementAction(recordFolder, "closeRecordFolder"); + return null; + } + }, AuthenticationUtil.getAdminUserName()); + } + public void freeze(final NodeRef nodeRef) { AuthenticationUtil.runAs(new RunAsWork() From 4c7c011ca8b3839ecf01645478e31e4e9ff9ebe4 Mon Sep 17 00:00:00 2001 From: Roy Wetherall Date: Wed, 18 Apr 2012 08:16:09 +0000 Subject: [PATCH 21/24] RM Bugs: - Images not showing in search results - Undeclared saved search showing record folders - Records eligable for destruction saved search is showing already ghosted records - rma:transferring aspect added to objects being transferred - Records eligable for transfer saved search is showing transferring and transfered records - Grandle explodedDeploy target updated to copy 'web' source git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/modules/recordsmanagement/HEAD@35352 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- build.gradle | 6 ++++++ .../module/org_alfresco_module_rm/model/recordsModel.xml | 5 +++++ .../module/org_alfresco_module_rm/rm-service-context.xml | 7 ++++--- .../org_alfresco_module_rm/action/impl/TransferAction.java | 7 +++++-- .../action/impl/TransferCompleteAction.java | 3 +++ .../model/RecordsManagementModel.java | 3 +++ 6 files changed, 26 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index bb38b225da..892d57931b 100644 --- a/build.gradle +++ b/build.gradle @@ -197,6 +197,7 @@ subprojects { if (explodedWebAppDir.exists() == true) { // copy module properties + // TODO but not so important for now // copy jars if (jarFileObj.exists()) { @@ -220,6 +221,11 @@ subprojects { // copy web if (sourceWebObj.exists() == true) { + copy { + from sourceWebObj + into "${explodedWebAppDir}" + + } } } else { diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/model/recordsModel.xml b/rm-server/config/alfresco/module/org_alfresco_module_rm/model/recordsModel.xml index a9a303abb1..139e1dbe3d 100644 --- a/rm-server/config/alfresco/module/org_alfresco_module_rm/model/recordsModel.xml +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/model/recordsModel.xml @@ -919,6 +919,11 @@ + + + + TransferringS + diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-service-context.xml b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-service-context.xml index a5ba27e4bf..696722cfdd 100644 --- a/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-service-context.xml +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-service-context.xml @@ -308,7 +308,8 @@ "search" : "ISNODE:T AND NOT ASPECT:\"rma:declaredRecord\"", "searchparams" : { - "records" : true, + "records" : true, + "recordfolders" : false, "undeclaredrecords" : true } }, @@ -325,7 +326,7 @@ { "name" : "Records Eligible For Transfer", "description" : "All records currently eligible for transfer.", - "search" : "dispositionActionName:\"transfer\" AND (dispositionEventsEligible:true OR dispositionActionAsOf:[MIN TO TODAY])", + "search" : "dispositionActionName:\"transfer\" AND (dispositionEventsEligible:true OR dispositionActionAsOf:[MIN TO TODAY]) AND NOT ASPECT:"rma:transferred" AND NOT ASPECT:"rma:transferring"", "searchparams" : { "records" : true, @@ -336,7 +337,7 @@ { "name" : "Records Eligible For Destruction", "description" : "All records currently eligible for destruction.", - "search" : "dispositionActionName:\"destroy\" AND (dispositionEventsEligible:true OR dispositionActionAsOf:[MIN TO TODAY])", + "search" : "dispositionActionName:\"destroy\" AND (dispositionEventsEligible:true OR dispositionActionAsOf:[MIN TO TODAY]) AND NOT ASPECT:\"rma:ghosted\"", "searchparams" : { "records" : true, diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/TransferAction.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/TransferAction.java index 57e8818b59..ba051b93b6 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/TransferAction.java +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/TransferAction.java @@ -104,7 +104,7 @@ public class TransferAction extends RMDispositionActionExecuterAbstractBase // Get the root rm node NodeRef root = this.recordsManagementService.getFilePlan(dispositionLifeCycleNodeRef); - // Get the hold object + // Get the transfer object NodeRef transferNodeRef = (NodeRef)AlfrescoTransactionSupport.getResource(KEY_TRANSFER_NODEREF); if (transferNodeRef == null) { @@ -139,7 +139,7 @@ public class TransferAction extends RMDispositionActionExecuterAbstractBase AlfrescoTransactionSupport.bindResource(KEY_TRANSFER_NODEREF, transferNodeRef); } - // Link the record to the hold + // Link the record to the trasnfer object this.nodeService.addChild(transferNodeRef, dispositionLifeCycleNodeRef, ASSOC_TRANSFERRED, @@ -148,6 +148,9 @@ public class TransferAction extends RMDispositionActionExecuterAbstractBase // Set PDF indicator flag setPDFIndicationFlag(transferNodeRef, dispositionLifeCycleNodeRef); + // Set the transferring indicator aspect + nodeService.addAspect(dispositionLifeCycleNodeRef, ASPECT_TRANSFERRING, null); + // Set the return value of the action action.setParameterValue(ActionExecuter.PARAM_RESULT, transferNodeRef); } diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/TransferCompleteAction.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/TransferCompleteAction.java index 4f7f2ba65f..5ed098a921 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/TransferCompleteAction.java +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/TransferCompleteAction.java @@ -116,6 +116,9 @@ public class TransferCompleteAction extends RMActionExecuterAbstractBase nodeService.setProperty(da.getNodeRef(), PROP_DISPOSITION_ACTION_COMPLETED_BY, AuthenticationUtil.getRunAsUser()); } + // Remove the transferring indicator aspect + nodeService.removeAspect(nodeRef, ASPECT_TRANSFERRING); + // Determine which marker aspect to use QName markerAspectQName = null; if (accessionIndicator == true) diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordsManagementModel.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordsManagementModel.java index 2f523e76ab..fcfa7c8816 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordsManagementModel.java +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordsManagementModel.java @@ -188,6 +188,9 @@ public interface RecordsManagementModel extends RecordsManagementCustomModel public static final QName PROP_TRANSFER_LOCATION = QName.createQName(RM_URI, "transferLocation"); public static final QName ASSOC_TRANSFERRED = QName.createQName(RM_URI, "transferred"); + // Transferring aspect + public static final QName ASPECT_TRANSFERRING = QName.createQName(RM_URI, "transferring"); + // Versioned record aspect public static final QName ASPECT_VERSIONED_RECORD = QName.createQName(RM_URI, "versionedRecord"); From 8414911186a8a2268c925eca4bac929cd81958ff Mon Sep 17 00:00:00 2001 From: Roy Wetherall Date: Thu, 19 Apr 2012 06:35:48 +0000 Subject: [PATCH 22/24] RM Bugs: - 'Create Reference' edit button now appears in UI (record picker is still broken) - Download action shouldn't show for nonElectronic documents - Delete action appears on frozen nodes - linked delete action in UI to delete capability git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/modules/recordsmanagement/HEAD@35406 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- .../rm-capabilities-context.xml | 4 +--- .../org_alfresco_module_rm/rm-service-context.xml | 2 +- .../rm-ui-evaluators-context.xml | 13 +++++++++++++ .../jscript/app/BaseEvaluator.java | 2 +- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-capabilities-context.xml b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-capabilities-context.xml index 0bdbf64c7b..65cd9ea80c 100644 --- a/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-capabilities-context.xml +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-capabilities-context.xml @@ -791,8 +791,6 @@ - - @@ -803,7 +801,7 @@ - + diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-service-context.xml b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-service-context.xml index 696722cfdd..f52e7a188d 100644 --- a/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-service-context.xml +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-service-context.xml @@ -326,7 +326,7 @@ { "name" : "Records Eligible For Transfer", "description" : "All records currently eligible for transfer.", - "search" : "dispositionActionName:\"transfer\" AND (dispositionEventsEligible:true OR dispositionActionAsOf:[MIN TO TODAY]) AND NOT ASPECT:"rma:transferred" AND NOT ASPECT:"rma:transferring"", + "search" : "dispositionActionName:\"transfer\" AND (dispositionEventsEligible:true OR dispositionActionAsOf:[MIN TO TODAY]) AND NOT ASPECT:\"rma:transferred\" AND NOT ASPECT:\"rma:transferring\"", "searchparams" : { "records" : true, diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-ui-evaluators-context.xml b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-ui-evaluators-context.xml index 98d1b16067..af7c65905d 100644 --- a/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-ui-evaluators-context.xml +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/rm-ui-evaluators-context.xml @@ -216,6 +216,19 @@ + + + + + RECORD_CATEGORY + RECORD_FOLDER + RECORD + + + + + diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/BaseEvaluator.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/BaseEvaluator.java index f1d3a15d22..014f4ad8e5 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/BaseEvaluator.java +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/BaseEvaluator.java @@ -213,7 +213,7 @@ public abstract class BaseEvaluator implements RecordsManagementModel Map accessStatus = capabilityService.getCapabilitiesAccessState(nodeRef, capabilities); for (AccessStatus value : accessStatus.values()) { - if (AccessStatus.ALLOWED.equals(value) == false) + if (AccessStatus.DENIED.equals(value) == true) { result = false; break; From 9955abc7dba0bd74b395193ed25dc971dd40fd50 Mon Sep 17 00:00:00 2001 From: Roy Wetherall Date: Thu, 19 Apr 2012 21:30:01 +0000 Subject: [PATCH 23/24] RM Bugs: * Create new reference now working * RM admin console link hidden untill RM site created git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/modules/recordsmanagement/HEAD@35442 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- .../jscript/app/JSONConversionComponent.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/JSONConversionComponent.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/JSONConversionComponent.java index 03ebff03d8..ee8ca3dcfb 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/JSONConversionComponent.java +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/JSONConversionComponent.java @@ -113,7 +113,10 @@ public class JSONConversionComponent extends org.alfresco.repo.jscript.app.JSONC // Get the 'kind' of the file plan component FilePlanComponentKind kind = recordsManagementService.getFilePlanComponentKind(nodeRef); - rmNodeValues.put("kind", kind.toString()); + rmNodeValues.put("kind", kind.toString()); + + // File plan node reference + rmNodeValues.put("filePlan", recordsManagementService.getFilePlan(nodeRef).toString()); // Set the indicators array setIndicators(rmNodeValues, nodeRef); From a2375a26fe64883a664e37c6668ffabd0f91ab4f Mon Sep 17 00:00:00 2001 From: Roy Wetherall Date: Thu, 19 Apr 2012 23:34:48 +0000 Subject: [PATCH 24/24] RM Build Scripts: * Reorganised parent and child property files so same values reused * Added build number to properties (included on artifact names) * Added simple root project task to bundle AMP's into zip for distribution (ready for QA) git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/modules/recordsmanagement/HEAD@35444 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- build.gradle | 24 +++++++++++++++++++++--- gradle.properties | 6 ++++-- rm-server/gradle.properties | 3 --- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index 892d57931b..fb03a8f61a 100644 --- a/build.gradle +++ b/build.gradle @@ -15,6 +15,25 @@ task wrapper(type: Wrapper) { gradleVersion = '1.0-milestone-8' } +task packageBuild (dependsOn: [':rm-server:amp', ':rm-share:amp']) << { + + distDir = file('dist') + if (distDir.exists() == false) { + distDir.mkdirs(); + } + + packageBaseName = "${groupid}-${packageName}-${version}-${build}" + packageZipFile = "${packageBaseName}.zip" + alfrescoAmp = "${project(':rm-server').name}/${project(':rm-server').buildDistDir}/${project(':rm-server').ampFile}" + shareAmp = "${project(':rm-share').name}/${project(':rm-share').buildDistDir}/${project(':rm-share').ampFile}" + + ant.zip(destfile: "${distDir}/${packageZipFile}", update: 'true') { + + ant.zipfileset(file: "${alfrescoAmp}") + ant.zipfileset(file: "${shareAmp}") + } +} + /** Subproject configuration */ subprojects { @@ -36,7 +55,7 @@ subprojects { configModuleDir = "config/alfresco/module/${moduleid}" moduleProperties = 'module.properties' fileMapping = 'file-mapping.properties' - baseName = "${groupid}-${appName}-${version}" + baseName = "${groupid}-${appName}-${version}-${build}" jarFile = "${baseName}.jar" ampFile = "${baseName}.amp" tomcatRoot = System.getenv(tomcatEnv) @@ -223,8 +242,7 @@ subprojects { if (sourceWebObj.exists() == true) { copy { from sourceWebObj - into "${explodedWebAppDir}" - + into "${explodedWebAppDir}" } } } diff --git a/gradle.properties b/gradle.properties index 294e20b6e8..a9ec5c46df 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,4 @@ -DIR_WAR=war -DIR_LIBS=war/WEB-INF/lib \ No newline at end of file +groupid=alfresco +packageName=rm +version=2.0.0 +build=1 \ No newline at end of file diff --git a/rm-server/gradle.properties b/rm-server/gradle.properties index 258a2334ac..1e54abeeba 100644 --- a/rm-server/gradle.properties +++ b/rm-server/gradle.properties @@ -1,7 +1,4 @@ -groupid=alfresco appName=rm -version=2.0 - moduleid=org_alfresco_module_rm webAppName=alfresco