/*
* #%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 <http://www.gnu.org/licenses/>.
* #L%
*/
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;
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.RecordsManagementPolicies;
import org.alfresco.module.org_alfresco_module_rm.RecordsManagementServiceRegistry;
import org.alfresco.module.org_alfresco_module_rm.disposition.property.DispositionProperty;
import org.alfresco.module.org_alfresco_module_rm.event.RecordsManagementEvent;
import org.alfresco.module.org_alfresco_module_rm.fileplan.FilePlanComponentKind;
import org.alfresco.module.org_alfresco_module_rm.fileplan.FilePlanService;
import org.alfresco.module.org_alfresco_module_rm.freeze.FreezeService;
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;
import org.alfresco.repo.policy.annotation.BehaviourKind;
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.cmr.repository.Period;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Disposition service implementation.
*
* @author Roy Wetherall
*/
@BehaviourBean
public class DispositionServiceImpl extends ServiceBaseImpl
implements DispositionService,
RecordsManagementModel,
RecordsManagementPolicies.OnFileRecord
{
/** 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;
/** File plan service */
private FilePlanService filePlanService;
/** Record Folder Service */
private RecordFolderService recordFolderService;
/** Record Service */
private RecordService recordService;
/** Freeze Service */
private FreezeService freezeService;
/** Disposition properties */
private Map<QName, DispositionProperty> dispositionProperties = new HashMap<QName, DispositionProperty>(4);
/**
* Set node service
*
* @param nodeService the node service
*/
@Override
public void setNodeService(NodeService nodeService)
{
this.nodeService = nodeService;
}
/**
* Set the dictionary service
*
* @param dictionaryServic the dictionary service
*/
@Override
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;
}
/**
* @param filePlanService file plan service
*/
public void setFilePlanService(FilePlanService filePlanService)
{
this.filePlanService = filePlanService;
}
/**
* @param recordFolderService record folder service
*/
public void setRecordFolderService(RecordFolderService recordFolderService)
{
this.recordFolderService = recordFolderService;
}
/**
* @param recordService record service
*/
public void setRecordService(RecordService recordService)
{
this.recordService = recordService;
}
/**
* @param freezeService freeze service
*/
public void setFreezeService(FreezeService freezeService)
{
this.freezeService = freezeService;
}
/**
* Behavior to initialize the disposition schedule of a newly filed record.
*
* @see org.alfresco.module.org_alfresco_module_rm.RecordsManagementPolicies.OnFileRecord#onFileRecord(org.alfresco.service.cmr.repository.NodeRef)
*/
@Override
@Behaviour(kind=BehaviourKind.CLASS, type="rma:record")
public void onFileRecord(NodeRef nodeRef)
{
// initialise disposition details
if (!nodeService.hasAspect(nodeRef, ASPECT_DISPOSITION_LIFECYCLE))
{
DispositionSchedule di = getDispositionSchedule(nodeRef);
if (di != null && di.isRecordLevelDisposition())
{
nodeService.addAspect(nodeRef, ASPECT_DISPOSITION_LIFECYCLE, null);
}
}
};
/**
* @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService#refreshDispositionAction(NodeRef)
*/
@Override
public void refreshDispositionAction(NodeRef nodeRef)
{
ParameterCheck.mandatory("nodeRef", nodeRef);
// get this disposition instructions for the node
DispositionSchedule di = getDispositionSchedule(nodeRef);
if (di != null)
{
List<DispositionActionDefinition> dispositionActionDefinitions = di.getDispositionActionDefinitions();
if (!dispositionActionDefinitions.isEmpty())
{
// get the first disposition action definition
DispositionActionDefinition nextDispositionActionDefinition = dispositionActionDefinitions.get(0);
// initialise the details of the next disposition action
initialiseDispositionAction(nodeRef, nextDispositionActionDefinition, true);
}
}
}
/** ========= Disposition Property Methods ========= */
/**
* @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService#registerDispositionProperty(org.alfresco.module.org_alfresco_module_rm.disposition.property.DispositionProperty)
*/
@Override
public void registerDispositionProperty(DispositionProperty dispositionProperty)
{
dispositionProperties.put(dispositionProperty.getQName(), dispositionProperty);
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService#getDispositionProperties(boolean, java.lang.String)
*/
@Override
public Collection<DispositionProperty> getDispositionProperties(boolean isRecordLevel, String dispositionAction)
{
Collection<DispositionProperty> values = dispositionProperties.values();
List<DispositionProperty> result = new ArrayList<DispositionProperty>(values.size());
for (DispositionProperty dispositionProperty : values)
{
boolean test = dispositionProperty.applies(isRecordLevel, dispositionAction);
if (test)
{
result.add(dispositionProperty);
}
}
return result;
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService#getDispositionProperties()
*/
@Override
public Collection<DispositionProperty> getDispositionProperties()
{
return dispositionProperties.values();
}
/** ========= Disposition Schedule Methods ========= */
/**
* @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService#getDispositionSchedule(org.alfresco.service.cmr.repository.NodeRef)
*/
@Override
public DispositionSchedule getDispositionSchedule(final NodeRef nodeRef)
{
DispositionSchedule ds = null;
NodeRef dsNodeRef = null;
if (isRecord(nodeRef))
{
// calculate disposition schedule without taking into account the user
DispositionSchedule originDispositionSchedule = AuthenticationUtil.runAsSystem(new RunAsWork<DispositionSchedule>()
{
@Override
public DispositionSchedule doWork()
{
return 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<Void>()
{
@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
dsNodeRef = getDispositionScheduleImpl(nodeRef);
}
if (dsNodeRef != null)
{
ds = new DispositionScheduleImpl(serviceRegistry, nodeService, dsNodeRef);
}
return ds;
}
/**
* 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 && filePlanService.isRecordCategory(parent))
{
result = getDispositionScheduleImpl(parent);
}
}
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)
*/
@Override
public DispositionSchedule getAssociatedDispositionSchedule(NodeRef nodeRef)
{
DispositionSchedule ds = null;
// Check the noderef parameter
ParameterCheck.mandatory("nodeRef", nodeRef);
if (nodeService.exists(nodeRef))
{
// 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 (!filePlanService.isFilePlanComponent(nodeRef))
{
throw new AlfrescoRuntimeException("Can not find the associated retention schedule for a non records management component. (nodeRef=" + nodeRef.toString() + ")");
}
if (this.nodeService.hasAspect(nodeRef, ASPECT_SCHEDULED))
{
List<ChildAssociationRef> 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))
{
List<ChildAssociationRef> 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())
{
LOGGER.warn("Retention 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)
*/
@Override
public List<NodeRef> 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<NodeRef> getDisposableItemsImpl(boolean isRecordLevelDisposition, NodeRef rmContainer)
{
List<NodeRef> items = filePlanService.getAllContained(rmContainer);
List<NodeRef> result = new ArrayList<NodeRef>(items.size());
for (NodeRef item : items)
{
if (recordFolderService.isRecordFolder(item))
{
if (isRecordLevelDisposition)
{
result.addAll(recordService.getRecords(item));
}
else
{
result.add(item);
}
}
else if (filePlanService.isRecordCategory(item) && 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<QName, Serializable> props)
{
NodeRef dsNodeRef = null;
// Check mandatory parameters
ParameterCheck.mandatory("nodeRef", nodeRef);
// Check exists
if (!nodeService.exists(nodeRef))
{
throw new AlfrescoRuntimeException("Unable to create retention 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) &&
!dictionaryService.isSubClass(nodeRefType, TYPE_RECORD_CATEGORY))
{
throw new AlfrescoRuntimeException("Unable to create retention 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))
{
nodeService.addAspect(nodeRef, ASPECT_SCHEDULED, null);
}
// Check whether there is already a disposition schedule object present
List<ChildAssociationRef> assocs = nodeService.getChildAssocs(nodeRef, ASSOC_DISPOSITION_SCHEDULE, RegexQNamePattern.MATCH_ALL);
if (assocs.size() == 0)
{
DispositionSchedule currentDispositionSchdule = getDispositionSchedule(nodeRef);
if (currentDispositionSchdule != null)
{
List<NodeRef> items = getDisposableItemsImpl(currentDispositionSchdule.isRecordLevelDisposition(), nodeRef);
if (items.size() != 0)
{
throw new AlfrescoRuntimeException("Can not create a retention schedule if there are disposable items already under the control of an other retention 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 retention schedule on node that already has a retention schedule.");
}
}
finally
{
behaviourFilter.enableBehaviour(nodeRef, ASPECT_SCHEDULED);
}
// Create the return object
return new DispositionScheduleImpl(serviceRegistry, nodeService, dsNodeRef);
}
/** ========= Disposition Action Definition Methods ========= */
/**
*
*/
@Override
public DispositionActionDefinition addDispositionActionDefinition(
DispositionSchedule schedule,
Map<QName, Serializable> 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)
*/
@Override
public void removeDispositionActionDefinition(DispositionSchedule schedule, DispositionActionDefinition actionDefinition)
{
// check first whether action definitions can be removed
if (hasDisposableItems(schedule))
{
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
*/
@Override
public DispositionActionDefinition updateDispositionActionDefinition(
DispositionActionDefinition actionDefinition,
Map<QName, Serializable> 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());
}
/** ========= Disposition Action Methods ========= */
/**
* Initialises the details of the next disposition action based on the details of a disposition
* action definition.
*
* @param nodeRef node reference
* @param dispositionActionDefinition disposition action definition
* @param allowContextFromAsOf true if the context date is allowed to be obtained from the disposition "as of" property.
*/
private DispositionAction initialiseDispositionAction(NodeRef nodeRef, DispositionActionDefinition dispositionActionDefinition, boolean allowContextFromAsOf)
{
List<ChildAssociationRef> 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<QName, Serializable> props = new HashMap<QName, Serializable>(10);
Date asOfDate = calculateAsOfDate(nodeRef, dispositionActionDefinition, allowContextFromAsOf);
// Set the property values
props.put(PROP_DISPOSITION_ACTION_ID, dispositionActionDefinition.getId());
props.put(PROP_DISPOSITION_ACTION, dispositionActionDefinition.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();
DispositionAction da = new DispositionActionImpl(serviceRegistry, dispositionActionNodeRef);
// Create the events
List<RecordsManagementEvent> events = dispositionActionDefinition.getEvents();
for (RecordsManagementEvent event : events)
{
// For every event create an entry on the action
da.addEventCompletionDetails(event);
}
return da;
}
/**
* Compute the "disposition as of" date (if necessary) for a disposition action and a node.
*
* @param nodeRef The node which the schedule applies to.
* @param dispositionActionDefinition The definition of the disposition action.
* @param allowContextFromAsOf true if the context date is allowed to be obtained from the disposition "as of" property.
* @return The new "disposition as of" date.
*/
@Override
public Date calculateAsOfDate(NodeRef nodeRef, DispositionActionDefinition dispositionActionDefinition,
boolean allowContextFromAsOf)
{
// Calculate the asOf date
Date asOfDate = null;
Period period = dispositionActionDefinition.getPeriod();
if (period != null)
{
Date contextDate = null;
// Get the period properties value
QName periodProperty = dispositionActionDefinition.getPeriodProperty();
if (periodProperty != null && (allowContextFromAsOf
|| !RecordsManagementModel.PROP_DISPOSITION_AS_OF.equals(periodProperty)))
{
// doesn't matter if the period property isn't set ... the asOfDate will get updated later
// when the value of the period property is set
contextDate = (Date)this.nodeService.getProperty(nodeRef, periodProperty);
}
else
{
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
if (contextDate != null)
{
asOfDate = period.getNextDate(contextDate);
}
}
return asOfDate;
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService#isNextDispositionActionEligible(org.alfresco.service.cmr.repository.NodeRef)
*/
@Override
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) &&
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()))
{
result = true;
}
if (!result)
{
DispositionAction da = new DispositionActionImpl(serviceRegistry, nextDa);
DispositionActionDefinition dad = da.getDispositionActionDefinition();
if (dad != null)
{
boolean firstComplete = dad.eligibleOnFirstCompleteEvent();
List<ChildAssociationRef> 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();
// implement AND and OR combination of event completions
if (isComplete)
{
result = true;
if (firstComplete)
{
break;
}
}
else
{
result = false;
if (!firstComplete)
{
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<ChildAssociationRef> assocs = nodeService.getChildAssocs(nodeRef, ASSOC_NEXT_DISPOSITION_ACTION, ASSOC_NEXT_DISPOSITION_ACTION, 1, true);
if (assocs.size() != 0)
{
result = assocs.get(0).getChildRef();
}
return result;
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService#getNextDispositionAction(org.alfresco.service.cmr.repository.NodeRef)
*/
@Override
public DispositionAction getNextDispositionAction(NodeRef nodeRef)
{
DispositionAction result = null;
NodeRef dispositionActionNodeRef = getNextDispositionActionNodeRef(nodeRef);
if (dispositionActionNodeRef != null)
{
result = new DispositionActionImpl(serviceRegistry, dispositionActionNodeRef);
}
return result;
}
/** ========= Disposition Action History Methods ========= */
/**
* @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService#getCompletedDispositionActions(org.alfresco.service.cmr.repository.NodeRef)
*/
@Override
public List<DispositionAction> getCompletedDispositionActions(NodeRef nodeRef)
{
List<ChildAssociationRef> assocs = nodeService.getChildAssocs(nodeRef, ASSOC_DISPOSITION_ACTION_HISTORY, RegexQNamePattern.MATCH_ALL);
List<DispositionAction> result = new ArrayList<DispositionAction>(assocs.size());
for (ChildAssociationRef assoc : assocs)
{
NodeRef dispositionActionNodeRef = assoc.getChildRef();
result.add(new DispositionActionImpl(serviceRegistry, dispositionActionNodeRef));
}
return result;
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService#getLastCompletedDispostionAction(org.alfresco.service.cmr.repository.NodeRef)
*/
@Override
public DispositionAction getLastCompletedDispostionAction(NodeRef nodeRef)
{
DispositionAction result = null;
List<DispositionAction> list = getCompletedDispositionActions(nodeRef);
if (!list.isEmpty())
{
// Get the last disposition action in the list
result = list.get(list.size()-1);
}
return result;
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService#isDisposableItemCutoff(NodeRef)
*/
@Override
public boolean isDisposableItemCutoff(NodeRef nodeRef)
{
ParameterCheck.mandatory("nodeRef", nodeRef);
return nodeService.hasAspect(nodeRef, ASPECT_CUT_OFF);
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService#updateNextDispositionAction(NodeRef)
*/
@Override
public void updateNextDispositionAction(final NodeRef nodeRef)
{
ParameterCheck.mandatory("nodeRef", nodeRef);
RunAsWork<Void> runAsWork = new RunAsWork<Void>()
{
/**
* @see org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork#doWork()
*/
@Override
public Void doWork()
{
// Get this disposition instructions for the node
DispositionSchedule di = getDispositionSchedule(nodeRef);
if (di != null)
{
// Get the current action node
NodeRef currentDispositionAction = null;
if (nodeService.hasAspect(nodeRef, ASPECT_DISPOSITION_LIFECYCLE))
{
List<ChildAssociationRef> assocs = nodeService.getChildAssocs(nodeRef, ASSOC_NEXT_DISPOSITION_ACTION, ASSOC_NEXT_DISPOSITION_ACTION);
if (assocs.size() > 0)
{
currentDispositionAction = assocs.get(0).getChildRef();
}
}
if (currentDispositionAction != null)
{
// Move it to the history association
nodeService.moveNode(currentDispositionAction, nodeRef, ASSOC_DISPOSITION_ACTION_HISTORY, ASSOC_DISPOSITION_ACTION_HISTORY);
}
List<DispositionActionDefinition> dispositionActionDefinitions = di.getDispositionActionDefinitions();
DispositionActionDefinition currentDispositionActionDefinition = null;
DispositionActionDefinition nextDispositionActionDefinition = null;
if (currentDispositionAction == null)
{
if (!dispositionActionDefinitions.isEmpty())
{
// The next disposition action is the first action
nextDispositionActionDefinition = dispositionActionDefinitions.get(0);
}
}
else
{
// 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)
{
String currentADName = (String) nodeService.getProperty(currentDispositionAction, PROP_DISPOSITION_ACTION);
currentDispositionActionDefinition = di.getDispositionActionDefinitionByName(currentADName);
}
// Get the next disposition action
int index = currentDispositionActionDefinition.getIndex();
index++;
if (index < dispositionActionDefinitions.size())
{
nextDispositionActionDefinition = dispositionActionDefinitions.get(index);
}
}
if (nextDispositionActionDefinition != null)
{
if (!nodeService.hasAspect(nodeRef, ASPECT_DISPOSITION_LIFECYCLE))
{
// Add the disposition life cycle aspect
nodeService.addAspect(nodeRef, ASPECT_DISPOSITION_LIFECYCLE, null);
}
initialiseDispositionAction(nodeRef, nextDispositionActionDefinition, false);
}
}
return null;
}
};
AuthenticationUtil.runAsSystem(runAsWork);
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService#cutoffDisposableItem(NodeRef)
*/
@Override
public void cutoffDisposableItem(final NodeRef nodeRef)
{
ParameterCheck.mandatory("nodeRef", nodeRef);
// check that the node ref is a filed record or record folder
if (FilePlanComponentKind.RECORD_FOLDER.equals(filePlanService.getFilePlanComponentKind(nodeRef)) ||
FilePlanComponentKind.RECORD.equals(filePlanService.getFilePlanComponentKind(nodeRef)))
{
if (!isDisposableItemCutoff(nodeRef) && !isFrozenOrHasFrozenChildren(nodeRef))
{
if (recordFolderService.isRecordFolder(nodeRef))
{
// cut off all the children first
for (NodeRef record : recordService.getRecords(nodeRef))
{
applyCutoff(record);
}
}
// apply cut off
applyCutoff(nodeRef);
// remove uncut off aspect if applied
if(nodeService.hasAspect(nodeRef, ASPECT_UNCUT_OFF))
{
nodeService.removeAspect(nodeRef, ASPECT_UNCUT_OFF);
}
// close the record folder if it isn't already closed!
if (recordFolderService.isRecordFolder(nodeRef) &&
!recordFolderService.isRecordFolderClosed(nodeRef))
{
// runAs system so that we can close a record that has already been cutoff
authenticationUtil.runAsSystem(new RunAsWork<Void>()
{
public Void doWork() throws Exception
{
recordFolderService.closeRecordFolder(nodeRef);
return null;
}
});
}
}
else
{
throw new AlfrescoRuntimeException("unable to perform cutoff, because node is frozen or has frozen children");
}
}
else
{
throw new AlfrescoRuntimeException("Unable to peform cutoff, because node is not a disposible item. (nodeRef=" + nodeRef.toString() + ")");
}
}
public Date getDispositionActionDate(NodeRef record, NodeRef dispositionSchedule, String dispositionActionName)
{
DispositionSchedule ds = new DispositionScheduleImpl(serviceRegistry, nodeService, dispositionSchedule);
List<ChildAssociationRef> 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;
}
public void recalculateNextDispositionStep(NodeRef record)
{
List<NodeRef> recordFolders = recordFolderService.getRecordFolders(record);
DispositionAction nextDispositionAction = getNextDispositionAction(record);
if (nextDispositionAction != null)
{
NextActionFromDisposition dsNextAction = getNextDispositionAction(record, recordFolders, nextDispositionAction);
if (dsNextAction != null)
{
final NodeRef action = dsNextAction.getNextActionNodeRef();
final Date dispositionActionDate = dsNextAction.getNextActionDateAsOf();
AuthenticationUtil.runAsSystem(new RunAsWork<Void>()
{
@Override
public Void doWork()
{
nodeService.setProperty(action, PROP_DISPOSITION_AS_OF, dispositionActionDate);
return null;
}
});
}
}
}
/**
* Helper method to determine if a node is frozen or has frozen children
*
* @param nodeRef Node to be checked
* @return <code>true</code> if the node is frozen or has frozen children, <code>false</code> otherwise
*/
private boolean isFrozenOrHasFrozenChildren(NodeRef nodeRef)
{
boolean result = false;
if (recordFolderService.isRecordFolder(nodeRef))
{
result = freezeService.isFrozen(nodeRef) || freezeService.hasFrozenChildren(nodeRef);
}
else if (recordService.isRecord(nodeRef))
{
result = freezeService.isFrozen(nodeRef);
}
else
{
throw new AlfrescoRuntimeException("The nodeRef '" + nodeRef + "' is neither a record nor a record folder.");
}
return result;
}
/**
* Helper method to apply the cut off
*
* @param nodeRef node to cut off
*/
private void applyCutoff(final NodeRef nodeRef)
{
AuthenticationUtil.runAsSystem(new RunAsWork<Void>()
{
@Override
public Void doWork()
{
// Apply the cut off aspect and set cut off date
Map<QName, Serializable> cutOffProps = new HashMap<QName, Serializable>(1);
cutOffProps.put(PROP_CUT_OFF_DATE, new Date());
nodeService.addAspect(nodeRef, ASPECT_CUT_OFF, cutOffProps);
return null;
}
});
}
/**
* 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<NodeRef> 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<NodeRef> 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<NodeRef> 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<DispositionActionDefinition> 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);
}
}