diff --git a/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/model/recordsModel.xml b/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/model/recordsModel.xml index ff24431ddc..70bec9a312 100644 --- a/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/model/recordsModel.xml +++ b/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/model/recordsModel.xml @@ -389,6 +389,11 @@ d:date false + + Manually Set Disposition Date Flag + d:boolean + false + Disposition Events Eligible d:boolean diff --git a/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/rm-service-context.xml b/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/rm-service-context.xml index 271b40b23c..97f6a97480 100644 --- a/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/rm-service-context.xml +++ b/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/rm-service-context.xml @@ -39,12 +39,6 @@ - - - - - @@ -96,9 +90,6 @@ - - - @@ -139,6 +130,7 @@ org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService.registerDispositionProperty=RM_ALLOW org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService.getDispositionProperties=RM_ALLOW org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService.getDispositionSchedule=RM.Read.0 + org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService.getOriginDispositionSchedule=RM.Read.0 org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService.getAssociatedDispositionSchedule=RM.Read.0 org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService.getAssociatedRecordsManagementContainer=RM_ALLOW org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService.hasDisposableItems=RM_ALLOW diff --git a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/BroadcastDispositionActionDefinitionUpdateAction.java b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/BroadcastDispositionActionDefinitionUpdateAction.java index c6b8db15b9..f8f660b3e8 100644 --- a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/BroadcastDispositionActionDefinitionUpdateAction.java +++ b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/BroadcastDispositionActionDefinitionUpdateAction.java @@ -27,6 +27,8 @@ package org.alfresco.module.org_alfresco_module_rm.action.impl; +import static org.apache.commons.lang3.BooleanUtils.isNotTrue; + import java.io.Serializable; import java.util.ArrayList; import java.util.Date; @@ -194,7 +196,8 @@ public class BroadcastDispositionActionDefinitionUpdateAction extends RMActionEx { // the change does effect the nextAction for this node // so go ahead and determine what needs updating - if (changedProps.contains(PROP_DISPOSITION_PERIOD)) + if (changedProps.contains(PROP_DISPOSITION_PERIOD) + && isNotTrue((Boolean) getNodeService().getProperty(nextAction.getNodeRef(), PROP_MANUALLY_SET_AS_OF))) { persistPeriodChanges(dispositionActionDef, nextAction); } diff --git a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/EditDispositionActionAsOfDateAction.java b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/EditDispositionActionAsOfDateAction.java index f1aa942dd5..43ae913bd3 100644 --- a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/EditDispositionActionAsOfDateAction.java +++ b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/action/impl/EditDispositionActionAsOfDateAction.java @@ -74,6 +74,7 @@ public class EditDispositionActionAsOfDateAction extends RMActionExecuterAbstrac if (da != null) { getNodeService().setProperty(da.getNodeRef(), PROP_DISPOSITION_AS_OF, asOfDate); + getNodeService().setProperty(da.getNodeRef(), PROP_MANUALLY_SET_AS_OF, true); } } else diff --git a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionService.java b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionService.java index cb6569dd67..4f412f7480 100644 --- a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionService.java +++ b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionService.java @@ -243,6 +243,17 @@ public interface DispositionService * @param nodeRef node reference */ void refreshDispositionAction(NodeRef nodeRef); + + /** + * Gets date of the disposition action for the given + * disposition schedule with the given action name + * + * @param record + * @param dispositionSchedule nodeRef + * @param dispositionActionName + * @return date + */ + Date getDispositionActionDate(NodeRef record, NodeRef dispositionSchedule, String dispositionActionName); /** * Compute the "disposition as of" date (if necessary) for a disposition action and a node. @@ -254,4 +265,13 @@ public interface DispositionService */ Date calculateAsOfDate(NodeRef nodeRef, DispositionActionDefinition dispositionActionDefinition, boolean allowContextFromAsOf); + + /** + * Gets the origin disposition schedule for the record, not the calculated one + * in case of multiple dispositions applied to record + * + * @param nodeRef record + * @return the initial disposition + */ + DispositionSchedule getOriginDispositionSchedule(NodeRef nodeRef); } diff --git a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionServiceImpl.java b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionServiceImpl.java index 6ec6da5757..29b1a5737b 100644 --- a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionServiceImpl.java +++ b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/DispositionServiceImpl.java @@ -27,6 +27,8 @@ package org.alfresco.module.org_alfresco_module_rm.disposition; +import static org.apache.commons.lang3.BooleanUtils.isNotTrue; + import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; @@ -36,6 +38,7 @@ 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.RecordsManagementPolicies; import org.alfresco.module.org_alfresco_module_rm.RecordsManagementServiceRegistry; import org.alfresco.module.org_alfresco_module_rm.disposition.property.DispositionProperty; @@ -47,6 +50,7 @@ import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; import org.alfresco.module.org_alfresco_module_rm.record.RecordService; import org.alfresco.module.org_alfresco_module_rm.recordfolder.RecordFolderService; import org.alfresco.module.org_alfresco_module_rm.util.ServiceBaseImpl; +import org.alfresco.repo.dictionary.types.period.Immediately; import org.alfresco.repo.policy.BehaviourFilter; import org.alfresco.repo.policy.annotation.Behaviour; import org.alfresco.repo.policy.annotation.BehaviourBean; @@ -79,15 +83,26 @@ public class DispositionServiceImpl extends ServiceBaseImpl /** Logger */ private static final Logger LOGGER = LoggerFactory.getLogger(DispositionServiceImpl.class); + /** Transaction mode for setting next action */ + public enum WriteMode + { + /** Do not update any data. */ + READ_ONLY, + /** Only set the "disposition as of" date. */ + DATE_ONLY, + /** + * Set the "disposition as of" date and the name of the next action. This only happens during the creation of a + * disposition schedule impl node under a record or folder. + */ + DATE_AND_NAME + }; + /** Behaviour filter */ private BehaviourFilter behaviourFilter; /** Records management service registry */ private RecordsManagementServiceRegistry serviceRegistry; - /** Disposition selection strategy */ - private DispositionSelectionStrategy dispositionSelectionStrategy; - /** File plan service */ private FilePlanService filePlanService; @@ -177,16 +192,6 @@ public class DispositionServiceImpl extends ServiceBaseImpl this.freezeService = freezeService; } - /** - * Set the dispositionSelectionStrategy bean. - * - * @param dispositionSelectionStrategy - */ - public void setDispositionSelectionStrategy(DispositionSelectionStrategy dispositionSelectionStrategy) - { - this.dispositionSelectionStrategy = dispositionSelectionStrategy; - } - /** * Behavior to initialize the disposition schedule of a newly filed record. * @@ -276,31 +281,68 @@ public class DispositionServiceImpl extends ServiceBaseImpl * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService#getDispositionSchedule(org.alfresco.service.cmr.repository.NodeRef) */ @Override - public DispositionSchedule getDispositionSchedule(NodeRef nodeRef) + public DispositionSchedule getDispositionSchedule(final NodeRef nodeRef) { - DispositionSchedule di = null; - NodeRef diNodeRef = null; + DispositionSchedule ds = null; + NodeRef dsNodeRef = null; if (isRecord(nodeRef)) { - // Get the record folders for the record - List recordFolders = recordFolderService.getRecordFolders(nodeRef); - // At this point, we may have disposition instruction objects from 1..n folders. - diNodeRef = dispositionSelectionStrategy.selectDispositionScheduleFrom(recordFolders); + DispositionSchedule originDispositionSchedule = getOriginDispositionSchedule(nodeRef); + // if the initial disposition schedule of the record is folder based + if (originDispositionSchedule == null || + isNotTrue(originDispositionSchedule.isRecordLevelDisposition())) + { + return null; + } + + final NextActionFromDisposition dsNextAction = getDispositionActionByNameForRecord(nodeRef); + + if (dsNextAction != null) + { + final NodeRef action = dsNextAction.getNextActionNodeRef(); + if (isNotTrue((Boolean)nodeService.getProperty(action, PROP_MANUALLY_SET_AS_OF))) + { + if (!dsNextAction.getWriteMode().equals(WriteMode.READ_ONLY)) + { + final String dispositionActionName = dsNextAction.getNextActionName(); + final Date dispositionActionDate = dsNextAction.getNextActionDateAsOf(); + + AuthenticationUtil.runAsSystem(new RunAsWork() + { + @Override + public Void doWork() + { + nodeService.setProperty(action, PROP_DISPOSITION_AS_OF, dispositionActionDate); + if (dsNextAction.getWriteMode().equals(WriteMode.DATE_AND_NAME)) + { + nodeService.setProperty(action, PROP_DISPOSITION_ACTION_NAME, dispositionActionName); + } + return null; + } + }); + } + } + + dsNodeRef = dsNextAction.getDispositionNodeRef(); + } } else { // Get the disposition instructions for the node reference provided - diNodeRef = getDispositionScheduleImpl(nodeRef); + dsNodeRef = getDispositionScheduleImpl(nodeRef); } - if (diNodeRef != null) + if (dsNodeRef != null) { - di = new DispositionScheduleImpl(serviceRegistry, nodeService, diNodeRef); + ds = new DispositionScheduleImpl(serviceRegistry, nodeService, dsNodeRef); } - return di; + return ds; } + + + /** * This method returns a NodeRef * Gets the disposition instructions @@ -322,6 +364,28 @@ public class DispositionServiceImpl extends ServiceBaseImpl } return result; } + + public DispositionSchedule getOriginDispositionSchedule(NodeRef nodeRef) + { + NodeRef parent = this.nodeService.getPrimaryParent(nodeRef).getParentRef(); + if (parent != null) + { + if (filePlanService.isRecordCategory(parent)) + { + NodeRef result = getAssociatedDispositionScheduleImpl(parent); + if (result == null) + { + return null; + } + return new DispositionScheduleImpl(serviceRegistry, nodeService, result); + } + else + { + return getOriginDispositionSchedule(parent); + } + } + return null; + } /** * @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService#getAssociatedDispositionSchedule(org.alfresco.service.cmr.repository.NodeRef) @@ -631,8 +695,14 @@ public class DispositionServiceImpl extends ServiceBaseImpl * @param dispositionActionDefinition disposition action definition * @param allowContextFromAsOf true if the context date is allowed to be obtained from the disposition "as of" property. */ - private void initialiseDispositionAction(NodeRef nodeRef, DispositionActionDefinition dispositionActionDefinition, boolean allowContextFromAsOf) + private DispositionAction initialiseDispositionAction(NodeRef nodeRef, DispositionActionDefinition dispositionActionDefinition, boolean allowContextFromAsOf) { + List childAssocs = nodeService.getChildAssocs(nodeRef, ASSOC_NEXT_DISPOSITION_ACTION, ASSOC_NEXT_DISPOSITION_ACTION, 1, true); + if (childAssocs != null && childAssocs.size() > 0) + { + return new DispositionActionImpl(serviceRegistry, childAssocs.get(0).getChildRef()); + } + // Create the properties Map props = new HashMap(10); @@ -662,6 +732,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl // For every event create an entry on the action da.addEventCompletionDetails(event); } + return da; } /** @@ -694,9 +765,16 @@ public class DispositionServiceImpl extends ServiceBaseImpl } else { - // for now use 'NOW' as the default context date - // TODO set the default period property ... cut off date or last disposition date depending on context - contextDate = new Date(); + if (period.getPeriodType().equals(Immediately.PERIOD_TYPE)) + { + contextDate = (Date)nodeService.getProperty(nodeRef, ContentModel.PROP_CREATED); + } + else + { + // for now use 'NOW' as the default context date + // TODO set the default period property ... cut off date or last disposition date depending on context + contextDate = new Date(); + } } // Calculate the as of date @@ -907,6 +985,7 @@ public class DispositionServiceImpl extends ServiceBaseImpl // Get the current action String currentADId = (String) nodeService.getProperty(currentDispositionAction, PROP_DISPOSITION_ACTION_ID); currentDispositionActionDefinition = di.getDispositionActionDefinition(currentADId); + // When the record has multiple disposition schedules the current disposition action may not be found by id // In this case it will be searched by name if(currentDispositionActionDefinition == null) @@ -1001,6 +1080,24 @@ public class DispositionServiceImpl extends ServiceBaseImpl } } + public Date getDispositionActionDate(NodeRef record, NodeRef dispositionSchedule, String dispositionActionName) + { + DispositionSchedule ds = new DispositionScheduleImpl(serviceRegistry, nodeService, dispositionSchedule); + List assocs = nodeService.getChildAssocs(dispositionSchedule); + if (assocs != null && assocs.size() > 0) + { + for (ChildAssociationRef assoc : assocs) + { + if (assoc != null && assoc.getQName().getLocalName().contains(dispositionActionName)) + { + DispositionActionDefinition actionDefinition = ds.getDispositionActionDefinition(assoc.getChildRef().getId()); + return calculateAsOfDate(record, actionDefinition, true); + } + } + } + return null; + } + /** * Helper method to determine if a node is frozen or has frozen children * @@ -1048,4 +1145,169 @@ public class DispositionServiceImpl extends ServiceBaseImpl } }); } + + + + /** + * Calculate next disposition action for a record + * + * @param record + * @return next disposition action (name, date) and the disposition associated + */ + + protected NextActionFromDisposition getDispositionActionByNameForRecord(NodeRef record) + { + List recordFolders = recordFolderService.getRecordFolders(record); + DispositionAction nextDispositionAction = getNextDispositionAction(record); + + if (nextDispositionAction == null) + { + DispositionAction lastCompletedDispositionAction = getLastCompletedDispostionAction(record); + if (lastCompletedDispositionAction != null) + { + // all disposition actions upon the given record were completed + return null; + } + + return getFirstDispositionAction(record, recordFolders); + } + else + { + return getNextDispositionAction(record, recordFolders, nextDispositionAction); + } + } + + /** + * Calculate next disposition action when the record already has one + * @param recordFolders + * @param nextDispositionAction + * @return next disposition action and the associated disposition schedule + */ + private NextActionFromDisposition getNextDispositionAction(NodeRef record, List recordFolders, DispositionAction nextDispositionAction) + { + String recordNextDispositionActionName = nextDispositionAction.getName(); + Date recordNextDispositionActionDate = nextDispositionAction.getAsOfDate(); + // We're looking for the latest date, so initially start with a very early one. + Date nextDispositionActionDate = new Date(Long.MIN_VALUE); + NodeRef dispositionNodeRef = null; + + // Find the latest "disposition as of" date from all the schedules this record is subject to. + for (NodeRef folder : recordFolders) + { + NodeRef dsNodeRef = getDispositionScheduleImpl(folder); + if (dsNodeRef != null) + { + Date dispActionDate = getDispositionActionDate(record, dsNodeRef, recordNextDispositionActionName); + if (dispActionDate == null || (nextDispositionActionDate != null + && nextDispositionActionDate.before(dispActionDate))) + { + nextDispositionActionDate = dispActionDate; + dispositionNodeRef = dsNodeRef; + if (dispActionDate == null) + { + // Treat null as the latest date possible (so stop searching further). + break; + } + } + } + } + if (dispositionNodeRef == null) + { + return null; + } + WriteMode mode = determineWriteMode(recordNextDispositionActionDate, nextDispositionActionDate); + + return new NextActionFromDisposition(dispositionNodeRef, nextDispositionAction.getNodeRef(), + recordNextDispositionActionName, nextDispositionActionDate, mode); + } + + /** + * Determine what should be updated for an existing disposition schedule impl. We only update the date if the + * existing date is earlier than the calculated one. + * + * @param recordNextDispositionActionDate The next action date found on the record node (or folder node). + * @param nextDispositionActionDate The next action date calculated from the current disposition schedule(s) + * affecting the node. + * @return READ_ONLY if nothing should be updated, or DATE_ONLY if the date needs updating. + */ + private WriteMode determineWriteMode(Date recordNextDispositionActionDate, Date nextDispositionActionDate) + { + // Treat null dates as being the latest possible date. + Date maxDate = new Date(Long.MAX_VALUE); + Date recordDate = (recordNextDispositionActionDate != null ? recordNextDispositionActionDate : maxDate); + Date calculatedDate = (nextDispositionActionDate != null ? nextDispositionActionDate : maxDate); + + // We only need to update the date if the current one is too early. + if (recordDate.before(calculatedDate)) + { + return WriteMode.DATE_ONLY; + } + else + { + return WriteMode.READ_ONLY; + } + } + + /** + * Calculate first disposition action when the record doesn't have one + * @param recordFolders + * @return next disposition action and the associated disposition schedule + */ + private NextActionFromDisposition getFirstDispositionAction(NodeRef record, List recordFolders) + { + NodeRef newAction = null; + String newDispositionActionName = null; + // We're looking for the latest date, so start with a very early one. + Date newDispositionActionDateAsOf = new Date(Long.MIN_VALUE); + NodeRef dispositionNodeRef = null; + for (NodeRef folder : recordFolders) + { + NodeRef folderDS = getDispositionScheduleImpl(folder); + if (folderDS != null) + { + DispositionSchedule ds = new DispositionScheduleImpl(serviceRegistry, nodeService, folderDS); + List dispositionActionDefinitions = ds.getDispositionActionDefinitions(); + + if (dispositionActionDefinitions != null && dispositionActionDefinitions.size() > 0) + { + DispositionActionDefinition firstDispositionActionDef = dispositionActionDefinitions.get(0); + dispositionNodeRef = folderDS; + + if (newAction == null) + { + NodeRef recordOrFolder = record; + if (!ds.isRecordLevelDisposition()) + { + recordOrFolder = folder; + } + DispositionAction firstDispositionAction = initialiseDispositionAction(recordOrFolder, firstDispositionActionDef, true); + newAction = firstDispositionAction.getNodeRef(); + newDispositionActionName = (String)nodeService.getProperty(newAction, PROP_DISPOSITION_ACTION_NAME); + newDispositionActionDateAsOf = firstDispositionAction.getAsOfDate(); + } + else if (firstDispositionActionDef.getPeriod() != null) + { + Date firstActionDate = calculateAsOfDate(record, firstDispositionActionDef, true); + if (firstActionDate == null || (newDispositionActionDateAsOf != null + && newDispositionActionDateAsOf.before(firstActionDate))) + { + newDispositionActionName = firstDispositionActionDef.getName(); + newDispositionActionDateAsOf = firstActionDate; + if (firstActionDate == null) + { + // Treat null as the latest date possible, so there's no point searching further. + break; + } + } + } + } + } + } + if (newDispositionActionName == null || dispositionNodeRef == null || newAction == null) + { + return null; + } + return new NextActionFromDisposition(dispositionNodeRef, newAction, + newDispositionActionName, newDispositionActionDateAsOf, WriteMode.DATE_AND_NAME); + } } diff --git a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/NextActionFromDisposition.java b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/NextActionFromDisposition.java new file mode 100644 index 0000000000..3054d39a09 --- /dev/null +++ b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/NextActionFromDisposition.java @@ -0,0 +1,106 @@ +/* + * #%L + * Alfresco Records Management Module + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * - + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * - + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * - + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * - + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.module.org_alfresco_module_rm.disposition; + +import java.util.Date; + +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionServiceImpl.WriteMode; +import org.alfresco.service.cmr.repository.NodeRef; + +public class NextActionFromDisposition +{ + public NextActionFromDisposition(NodeRef dispositionNodeRef, NodeRef nextActionNodeRef, String nextActionName, Date nextActionDateAsOf, + WriteMode writeMode) + { + super(); + this.dispositionNodeRef = dispositionNodeRef; + this.nextActionNodeRef = nextActionNodeRef; + this.nextActionName = nextActionName; + this.nextActionDateAsOf = nextActionDateAsOf; + this.writeMode = writeMode; + } + + private NodeRef dispositionNodeRef; + + private NodeRef nextActionNodeRef; + + private String nextActionName; + + private Date nextActionDateAsOf; + + private WriteMode writeMode; + + public WriteMode getWriteMode() + { + return writeMode; + } + + public void setWriteMode(WriteMode writeMode) + { + this.writeMode = writeMode; + } + + public NodeRef getNextActionNodeRef() + { + return nextActionNodeRef; + } + + public void setNextActionNodeRef(NodeRef nextActionNodeRef) + { + this.nextActionNodeRef = nextActionNodeRef; + } + + public NodeRef getDispositionNodeRef() + { + return dispositionNodeRef; + } + + public void setDispositionNodeRef(NodeRef dispositionNodeRef) + { + this.dispositionNodeRef = dispositionNodeRef; + } + + public String getNextActionName() + { + return nextActionName; + } + + public void setNextActionName(String nextActionName) + { + this.nextActionName = nextActionName; + } + + public Date getNextActionDateAsOf() + { + return nextActionDateAsOf; + } + + public void setNextActionDateAsOf(Date nextActionDateAsOf) + { + this.nextActionDateAsOf = nextActionDateAsOf; + } +} diff --git a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/property/DispositionProperty.java b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/property/DispositionProperty.java index 310486d130..fad60799e2 100644 --- a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/property/DispositionProperty.java +++ b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/disposition/property/DispositionProperty.java @@ -27,6 +27,8 @@ package org.alfresco.module.org_alfresco_module_rm.disposition.property; +import static org.apache.commons.lang3.BooleanUtils.isNotTrue; + import java.io.Serializable; import java.util.Date; import java.util.Map; @@ -220,7 +222,11 @@ public class DispositionProperty extends BaseBehaviourBean // update asOf date on the disposition action based on the new property value NodeRef daNodeRef = dispositionAction.getNodeRef(); - nodeService.setProperty(daNodeRef, PROP_DISPOSITION_AS_OF, updatedAsOf); + // Don't overwrite a manually set "disposition as of" date. + if (isNotTrue((Boolean) nodeService.getProperty(daNodeRef, PROP_MANUALLY_SET_AS_OF))) + { + nodeService.setProperty(daNodeRef, PROP_DISPOSITION_AS_OF, updatedAsOf); + } } } } diff --git a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordsManagementModel.java b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordsManagementModel.java index 082b626752..ee8a2a8acd 100644 --- a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordsManagementModel.java +++ b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/model/RecordsManagementModel.java @@ -160,6 +160,8 @@ public interface RecordsManagementModel extends RecordsManagementCustomModel QName PROP_DISPOSITION_ACTION_ID = QName.createQName(RM_URI, "dispositionActionId"); QName PROP_DISPOSITION_ACTION = QName.createQName(RM_URI, "dispositionAction"); QName PROP_DISPOSITION_AS_OF = QName.createQName(RM_URI, "dispositionAsOf"); + /** A flag indicating that the "disposition as of" date has been manually set and shouldn't be changed. */ + QName PROP_MANUALLY_SET_AS_OF = QName.createQName(RM_URI, "manuallySetAsOf"); QName PROP_DISPOSITION_EVENTS_ELIGIBLE = QName.createQName(RM_URI, "dispositionEventsEligible"); QName PROP_DISPOSITION_ACTION_STARTED_AT = QName.createQName(RM_URI, "dispositionActionStartedAt"); QName PROP_DISPOSITION_ACTION_STARTED_BY = QName.createQName(RM_URI, "dispositionActionStartedBy"); diff --git a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/record/RecordServiceImpl.java b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/record/RecordServiceImpl.java index 134811f0f2..d8e9f295a3 100644 --- a/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/record/RecordServiceImpl.java +++ b/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/record/RecordServiceImpl.java @@ -45,12 +45,14 @@ import java.util.Set; import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.RecordsManagementServiceRegistry; import org.alfresco.module.org_alfresco_module_rm.RecordsManagementPolicies.BeforeFileRecord; import org.alfresco.module.org_alfresco_module_rm.RecordsManagementPolicies.OnFileRecord; 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.RMPermissionModel; 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.fileplan.FilePlanService; @@ -241,7 +243,7 @@ public class RecordServiceImpl extends BaseBehaviourBean /** recordable version service */ private RecordableVersionService recordableVersionService; - + /** list of available record meta-data aspects and the file plan types the are applicable to */ private Map> recordMetaDataAspects; @@ -398,7 +400,7 @@ public class RecordServiceImpl extends BaseBehaviourBean { this.recordableVersionService = recordableVersionService; } - + /** * Init method */ @@ -1753,7 +1755,10 @@ public class RecordServiceImpl extends BaseBehaviourBean private void validateLinkConditions(NodeRef record, NodeRef recordFolder) { // ensure that the linking record folders have compatible disposition schedules - DispositionSchedule recordDispositionSchedule = dispositionService.getDispositionSchedule(record); + + // get the origin disposition schedule for the record, not the calculated one + DispositionSchedule recordDispositionSchedule = dispositionService.getOriginDispositionSchedule(record); + if (recordDispositionSchedule != null) { DispositionSchedule recordFolderDispositionSchedule = dispositionService.getDispositionSchedule(recordFolder); diff --git a/rm-community/rm-community-repo/test/java/org/alfresco/module/org_alfresco_module_rm/test/integration/disposition/MultipleSchedulesTest.java b/rm-community/rm-community-repo/test/java/org/alfresco/module/org_alfresco_module_rm/test/integration/disposition/MultipleSchedulesTest.java new file mode 100644 index 0000000000..daa9ab1818 --- /dev/null +++ b/rm-community/rm-community-repo/test/java/org/alfresco/module/org_alfresco_module_rm/test/integration/disposition/MultipleSchedulesTest.java @@ -0,0 +1,208 @@ +/* + * #%L + * Alfresco Records Management Module + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * - + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * - + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * - + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * - + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.module.org_alfresco_module_rm.test.integration.disposition; + +import static org.alfresco.module.org_alfresco_module_rm.test.util.bdt.BehaviourTest.test; + +import java.io.Serializable; +import java.util.Calendar; +import java.util.Date; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.module.org_alfresco_module_rm.action.impl.CutOffAction; +import org.alfresco.module.org_alfresco_module_rm.action.impl.DestroyAction; +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.test.util.BaseRMTestCase; +import org.alfresco.module.org_alfresco_module_rm.test.util.CommonRMTestUtils; +import org.alfresco.module.org_alfresco_module_rm.test.util.bdt.BehaviourTest; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.ApplicationContextHelper; +import org.springframework.extensions.webscripts.GUID; + +import com.google.common.collect.ImmutableMap; + +/** + * Integration tests for records linked to multiple disposition schedules. + * + * @author Tom Page + * @since 2.3.1 + */ +public class MultipleSchedulesTest extends BaseRMTestCase +{ + /** A unique prefix for the constants in this test. */ + protected static final String TEST_PREFIX = MultipleSchedulesTest.class.getName() + GUID.generate() + "_"; + /** The name to use for the first category. */ + protected static final String CATEGORY_A_NAME = TEST_PREFIX + "CategoryA"; + /** The name to use for the folder within the first category. */ + protected static final String FOLDER_A_NAME = TEST_PREFIX + "FolderA"; + /** The name to use for the second category. */ + protected static final String CATEGORY_B_NAME = TEST_PREFIX + "CategoryB"; + /** The name to use for the folder within the second category. */ + protected static final String FOLDER_B_NAME = TEST_PREFIX + "FolderB"; + /** The name to use for the record. */ + protected static final String RECORD_NAME = TEST_PREFIX + "Record"; + + /** The internal disposition service is used to avoid permissions issues when updating the record. */ + private DispositionService internalDispositionService; + + /** The first category node. */ + private NodeRef categoryA; + /** The folder node within the first category. */ + private NodeRef folderA; + /** The second category node. */ + private NodeRef categoryB; + /** The folder node within the second category. */ + private NodeRef folderB; + /** The record node. */ + private NodeRef record; + + @Override + protected void setUp() throws Exception + { + super.setUp(); + + BehaviourTest.initBehaviourTests(retryingTransactionHelper); + + // Get the application context + applicationContext = ApplicationContextHelper.getApplicationContext(getConfigLocations()); + internalDispositionService = (DispositionService) applicationContext.getBean("dispositionService"); + + // Ensure different records are used for each test. + record = null; + } + + /** + * Create two categories each containing a folder. Set up a schedule on category A that applies to records (cutoff + * immediately, destroy immediately). Set up a schedule on category B that is the same, but with a week delay before + * destroy becomes eligible. + */ + private void setUpFilePlan() + { + // Only set up the file plan if it hasn't already been done. + if (categoryA != null) + { + return; + } + + // Create two categories. + categoryA = filePlanService.createRecordCategory(filePlan, CATEGORY_A_NAME); + categoryB = filePlanService.createRecordCategory(filePlan, CATEGORY_B_NAME); + // Create a disposition schedule for category A (Cut off immediately, then Destroy immediately). + DispositionSchedule dispSchedA = utils.createBasicDispositionSchedule(categoryA, "instructions", "authority", true, false); + Map cutOffParamsA = ImmutableMap.of(PROP_DISPOSITION_ACTION_NAME, CutOffAction.NAME, + PROP_DISPOSITION_DESCRIPTION, "description", + PROP_DISPOSITION_PERIOD, CommonRMTestUtils.PERIOD_IMMEDIATELY); + dispositionService.addDispositionActionDefinition(dispSchedA, cutOffParamsA); + Map destroyParamsA = ImmutableMap.of(PROP_DISPOSITION_ACTION_NAME, DestroyAction.NAME, + PROP_DISPOSITION_DESCRIPTION, "description", + PROP_DISPOSITION_PERIOD, CommonRMTestUtils.PERIOD_IMMEDIATELY); + dispositionService.addDispositionActionDefinition(dispSchedA, destroyParamsA); + // Create a disposition schedule for category B (Cut off immediately, then Destroy one week after cutoff). + DispositionSchedule dispSchedB = utils.createBasicDispositionSchedule(categoryB, "instructions", "authority", true, false); + Map cutOffParamsB = ImmutableMap.of(PROP_DISPOSITION_ACTION_NAME, CutOffAction.NAME, + PROP_DISPOSITION_DESCRIPTION, "description", + PROP_DISPOSITION_PERIOD, CommonRMTestUtils.PERIOD_IMMEDIATELY); + dispositionService.addDispositionActionDefinition(dispSchedB, cutOffParamsB); + Map destroyParamsB = ImmutableMap.of(PROP_DISPOSITION_ACTION_NAME, DestroyAction.NAME, + PROP_DISPOSITION_DESCRIPTION, "description", + PROP_DISPOSITION_PERIOD, CommonRMTestUtils.PERIOD_ONE_WEEK, + PROP_DISPOSITION_PERIOD_PROPERTY, PROP_CUT_OFF_DATE); + dispositionService.addDispositionActionDefinition(dispSchedB, destroyParamsB); + // Create a folder within each category. + folderA = recordFolderService.createRecordFolder(categoryA, FOLDER_A_NAME); + folderB = recordFolderService.createRecordFolder(categoryB, FOLDER_B_NAME); + } + + /** + * RM-2526 + *

+     * Given a record subject to a disposition schedule
+     * And it is linked to a disposition schedule with the same step order, but a longer destroy step
+     * When the record is moved onto the destroy step
+     * Then the "as of" date is calculated using the longer period.
+     * 
+ */ + public void testLinkedToLongerSchedule() + { + Calendar calendar = Calendar.getInstance(); + test() + .given(() -> { + setUpFilePlan(); + // Create a record filed under category A and linked to category B. + record = fileFolderService.create(folderA, RECORD_NAME, ContentModel.TYPE_CONTENT).getNodeRef(); + recordService.link(record, folderB); + }) + .when(() -> { + // Cut off the record. + dispositionService.cutoffDisposableItem(record); + // Ensure the update has been applied to the record. + internalDispositionService.updateNextDispositionAction(record); + calendar.setTime((Date) nodeService.getProperty(record, PROP_CUT_OFF_DATE)); + calendar.add(Calendar.WEEK_OF_YEAR, 1); + }) + .then() + .expect(calendar.getTime()) + .from(() -> dispositionService.getNextDispositionAction(record).getAsOfDate()) + .because("Record should follow largest rentention schedule period, which is one week."); + } + + /** + * RM-2526 + *

+     * Given a record subject to a disposition schedule
+     * And it is linked to a disposition schedule with the same step order, but a shorter destroy step
+     * When the record is moved onto the destroy step
+     * Then the "as of" date is calculated using the longer period.
+     * 
+ */ + public void testLinkedToShorterSchedule() + { + Calendar calendar = Calendar.getInstance(); + test() + .given(() -> { + setUpFilePlan(); + // Create a record filed under category B and linked to category A. + record = fileFolderService.create(folderB, RECORD_NAME, ContentModel.TYPE_CONTENT).getNodeRef(); + recordService.link(record, folderA); + }) + .when(() -> { + // Cut off the record. + dispositionService.cutoffDisposableItem(record); + // Ensure the update has been applied to the record. + internalDispositionService.updateNextDispositionAction(record); + calendar.setTime((Date) nodeService.getProperty(record, PROP_CUT_OFF_DATE)); + calendar.add(Calendar.WEEK_OF_YEAR, 1); + }) + .then() + .expect(calendar.getTime()) + .from(() -> dispositionService.getNextDispositionAction(record).getAsOfDate()) + .because("Record should follow largest rentention schedule period, which is one week."); + } +} diff --git a/rm-community/rm-community-repo/test/java/org/alfresco/module/org_alfresco_module_rm/test/integration/disposition/UpdateNextDispositionActionTest.java b/rm-community/rm-community-repo/test/java/org/alfresco/module/org_alfresco_module_rm/test/integration/disposition/UpdateNextDispositionActionTest.java index 462198f5a7..355d9c7eef 100644 --- a/rm-community/rm-community-repo/test/java/org/alfresco/module/org_alfresco_module_rm/test/integration/disposition/UpdateNextDispositionActionTest.java +++ b/rm-community/rm-community-repo/test/java/org/alfresco/module/org_alfresco_module_rm/test/integration/disposition/UpdateNextDispositionActionTest.java @@ -44,6 +44,7 @@ import org.alfresco.module.org_alfresco_module_rm.action.impl.EditDispositionAct import org.alfresco.module.org_alfresco_module_rm.action.impl.TransferAction; import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionSchedule; 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; @@ -51,7 +52,7 @@ import org.alfresco.service.namespace.QName; * Update next disposition step integration tests. * * @author Roxana Lucanu -* @since 2.4.1 +* @since 2.3.1 */ public class UpdateNextDispositionActionTest extends BaseRMTestCase { @@ -103,11 +104,6 @@ public class UpdateNextDispositionActionTest extends BaseRMTestCase // complete record utils.completeRecord(record); - // set the disposition as of date to now on the record - rmActionService.executeRecordsManagementAction(record, - EditDispositionActionAsOfDateAction.NAME, - Collections.singletonMap(EditDispositionActionAsOfDateAction.PARAM_AS_OF_DATE, new Date())); - // cut off rmActionService.executeRecordsManagementAction(record, CutOffAction.NAME, null); } @@ -115,7 +111,7 @@ public class UpdateNextDispositionActionTest extends BaseRMTestCase @Override public void then() throws Exception { - assertTrue(nodeService.hasAspect(record, ASPECT_CUT_OFF)); + assertTrue("Record " + record + " doesn't have the cutOff aspect.", nodeService.hasAspect(record, ASPECT_CUT_OFF)); } }); } @@ -128,7 +124,7 @@ public class UpdateNextDispositionActionTest extends BaseRMTestCase Map cutOff = new HashMap(3); cutOff.put(PROP_DISPOSITION_ACTION_NAME, CutOffAction.NAME); cutOff.put(PROP_DISPOSITION_DESCRIPTION, generate()); - cutOff.put(PROP_DISPOSITION_PERIOD, PERIOD_ONE_WEEK); + cutOff.put(PROP_DISPOSITION_PERIOD, CommonRMTestUtils.PERIOD_IMMEDIATELY); dispositionService.addDispositionActionDefinition(ds, cutOff); // create the properties for TRANSFER action and add it to the disposition action definition diff --git a/rm-community/rm-community-repo/test/java/org/alfresco/module/org_alfresco_module_rm/test/legacy/service/DispositionServiceImplTest.java b/rm-community/rm-community-repo/test/java/org/alfresco/module/org_alfresco_module_rm/test/legacy/service/DispositionServiceImplTest.java index 65721473d3..5973378a60 100644 --- a/rm-community/rm-community-repo/test/java/org/alfresco/module/org_alfresco_module_rm/test/legacy/service/DispositionServiceImplTest.java +++ b/rm-community/rm-community-repo/test/java/org/alfresco/module/org_alfresco_module_rm/test/legacy/service/DispositionServiceImplTest.java @@ -666,7 +666,7 @@ public class DispositionServiceImplTest extends BaseRMTestCase checkDisposableItemChanged(mhRecordFolder42); checkDisposableItemChanged(record43); checkDisposableItemUnchanged(mhRecordFolder44); - checkDisposableItemUnchanged(record45);; + checkDisposableItemUnchanged(record45); } }); diff --git a/rm-community/rm-community-repo/unit-test/java/org/alfresco/module/org_alfresco_module_rm/record/RecordServiceImplUnitTest.java b/rm-community/rm-community-repo/unit-test/java/org/alfresco/module/org_alfresco_module_rm/record/RecordServiceImplUnitTest.java index 506b8c03da..1bc023392e 100644 --- a/rm-community/rm-community-repo/unit-test/java/org/alfresco/module/org_alfresco_module_rm/record/RecordServiceImplUnitTest.java +++ b/rm-community/rm-community-repo/unit-test/java/org/alfresco/module/org_alfresco_module_rm/record/RecordServiceImplUnitTest.java @@ -274,7 +274,7 @@ public class RecordServiceImplUnitTest extends BaseUnitTest DispositionSchedule recordDispositionSchedule = mock(DispositionSchedule.class); when(recordDispositionSchedule.isRecordLevelDisposition()) .thenReturn(true); - when(mockedDispositionService.getDispositionSchedule(record)) + when(mockedDispositionService.getOriginDispositionSchedule(record)) .thenReturn(recordDispositionSchedule); DispositionSchedule recordFolderDispositionSchedule = mock(DispositionSchedule.class);