/*
* #%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.record;
import static com.google.common.collect.Lists.newArrayList;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.model.ContentModel;
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.DispositionService;
import org.alfresco.module.org_alfresco_module_rm.dod5015.DOD5015Model;
import org.alfresco.module.org_alfresco_module_rm.fileplan.FilePlanService;
import org.alfresco.module.org_alfresco_module_rm.identifier.IdentifierService;
import org.alfresco.module.org_alfresco_module_rm.model.BaseBehaviourBean;
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.rma.type.RecordsManagementContainerType;
import org.alfresco.module.org_alfresco_module_rm.model.security.ModelAccessDeniedException;
import org.alfresco.module.org_alfresco_module_rm.notification.RecordsManagementNotificationHelper;
import org.alfresco.module.org_alfresco_module_rm.recordfolder.RecordFolderService;
import org.alfresco.module.org_alfresco_module_rm.relationship.RelationshipService;
import org.alfresco.module.org_alfresco_module_rm.report.ReportModel;
import org.alfresco.module.org_alfresco_module_rm.role.FilePlanRoleService;
import org.alfresco.module.org_alfresco_module_rm.role.Role;
import org.alfresco.module.org_alfresco_module_rm.security.ExtendedSecurityService;
import org.alfresco.module.org_alfresco_module_rm.version.RecordableVersionModel;
import org.alfresco.module.org_alfresco_module_rm.version.RecordableVersionService;
import org.alfresco.repo.node.NodeServicePolicies;
import org.alfresco.repo.policy.Behaviour.NotificationFrequency;
import org.alfresco.repo.policy.ClassPolicyDelegate;
import org.alfresco.repo.policy.JavaBehaviour;
import org.alfresco.repo.policy.PolicyComponent;
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.repo.security.permissions.AccessDeniedException;
import org.alfresco.repo.security.permissions.impl.ExtendedPermissionService;
import org.alfresco.service.cmr.dictionary.AspectDefinition;
import org.alfresco.service.cmr.dictionary.ClassDefinition;
import org.alfresco.service.cmr.dictionary.PropertyDefinition;
import org.alfresco.service.cmr.model.FileExistsException;
import org.alfresco.service.cmr.model.FileFolderService;
import org.alfresco.service.cmr.model.FileInfo;
import org.alfresco.service.cmr.model.FileNotFoundException;
import org.alfresco.service.cmr.repository.AssociationRef;
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.ContentWriter;
import org.alfresco.service.cmr.repository.InvalidNodeRefException;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.rule.RuleService;
import org.alfresco.service.cmr.security.AccessPermission;
import org.alfresco.service.cmr.security.AccessStatus;
import org.alfresco.service.cmr.security.OwnableService;
import org.alfresco.service.cmr.security.PermissionService;
import org.alfresco.service.cmr.version.Version;
import org.alfresco.service.cmr.version.VersionHistory;
import org.alfresco.service.cmr.version.VersionService;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.namespace.RegexQNamePattern;
import org.alfresco.util.EqualsHelper;
import org.alfresco.util.Pair;
import org.alfresco.util.ParameterCheck;
import org.alfresco.util.PropertyMap;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.extensions.surf.util.I18NUtil;
/**
* Record service implementation.
*
* @author Roy Wetherall
* @since 2.1
*/
@BehaviourBean
public class RecordServiceImpl extends BaseBehaviourBean
implements RecordService,
RecordsManagementModel,
RecordsManagementCustomModel,
NodeServicePolicies.OnCreateChildAssociationPolicy,
NodeServicePolicies.OnAddAspectPolicy,
NodeServicePolicies.OnRemoveAspectPolicy,
NodeServicePolicies.OnUpdatePropertiesPolicy
{
/** Logger */
private static Log logger = LogFactory.getLog(RecordServiceImpl.class);
/** transation data key */
private static final String KEY_IGNORE_ON_UPDATE = "ignoreOnUpdate";
private static final String KEY_PENDING_FILLING = "pendingFilling";
public static final String KEY_NEW_RECORDS = "newRecords";
/** I18N */
private static final String MSG_NODE_HAS_ASPECT = "rm.service.node-has-aspect";
private static final String FINAL_VERSION = "rm.service.final-version";
private static final String FINAL_DESCRIPTION = "rm.service.final-version-description";
/** Always edit property array */
private static final QName[] ALWAYS_EDIT_PROPERTIES = new QName[]
{
ContentModel.PROP_LAST_THUMBNAIL_MODIFICATION_DATA
};
/** always edit model URI's */
protected List<String> getAlwaysEditURIs()
{
return newArrayList(
NamespaceService.SECURITY_MODEL_1_0_URI,
NamespaceService.SYSTEM_MODEL_1_0_URI,
NamespaceService.WORKFLOW_MODEL_1_0_URI,
NamespaceService.APP_MODEL_1_0_URI,
NamespaceService.DATALIST_MODEL_1_0_URI,
NamespaceService.DICTIONARY_MODEL_1_0_URI,
NamespaceService.BPM_MODEL_1_0_URI,
NamespaceService.RENDITION_MODEL_1_0_URI
);
}
/** record model URI's */
public static final List<String> RECORD_MODEL_URIS = Collections.unmodifiableList(
Arrays.asList(
RM_URI,
RM_CUSTOM_URI,
ReportModel.RMR_URI,
RecordableVersionModel.RMV_URI,
DOD5015Model.DOD_URI
));
/** non-record model URI's */
private static final String[] NON_RECORD_MODEL_URIS = new String[]
{
NamespaceService.AUDIO_MODEL_1_0_URI,
NamespaceService.CONTENT_MODEL_1_0_URI,
NamespaceService.EMAILSERVER_MODEL_URI,
NamespaceService.EXIF_MODEL_1_0_URI,
NamespaceService.FORUMS_MODEL_1_0_URI,
NamespaceService.LINKS_MODEL_1_0_URI,
NamespaceService.REPOSITORY_VIEW_1_0_URI
};
/** Indentity service */
private IdentifierService identifierService;
/** Extended permission service */
private ExtendedPermissionService extendedPermissionService;
/** Extended security service */
private ExtendedSecurityService extendedSecurityService;
/** File plan service */
private FilePlanService filePlanService;
/** Records management notification helper */
private RecordsManagementNotificationHelper notificationHelper;
/** Policy component */
private PolicyComponent policyComponent;
/** Ownable service */
private OwnableService ownableService;
/** Capability service */
private CapabilityService capabilityService;
/** Rule service */
private RuleService ruleService;
/** File folder service */
private FileFolderService fileFolderService;
/** Record folder service */
private RecordFolderService recordFolderService;
/** File plan role service */
private FilePlanRoleService filePlanRoleService;
/** Permission service */
private PermissionService permissionService;
/** Version service */
private VersionService versionService;
/** Relationship service */
private RelationshipService relationshipService;
/** Disposition service */
private DispositionService dispositionService;
/** records management container type */
private RecordsManagementContainerType recordsManagementContainerType;
/** recordable version service */
private RecordableVersionService recordableVersionService;
/** list of available record meta-data aspects and the file plan types the are applicable to */
private Map<QName, Set<QName>> recordMetaDataAspects;
/** policies */
private ClassPolicyDelegate<BeforeFileRecord> beforeFileRecord;
private ClassPolicyDelegate<OnFileRecord> onFileRecord;
/** Behaviours */
private JavaBehaviour onCreateChildAssociation = new JavaBehaviour(
this,
"onCreateChildAssociation",
NotificationFrequency.FIRST_EVENT);
/**
* @param identifierService identifier service
*/
public void setIdentifierService(IdentifierService identifierService)
{
this.identifierService = identifierService;
}
/**
* @param extendedPermissionService extended permission service
*/
public void setExtendedPermissionService(ExtendedPermissionService extendedPermissionService)
{
this.extendedPermissionService = extendedPermissionService;
}
/**
* @param extendedSecurityService extended security service
*/
public void setExtendedSecurityService(ExtendedSecurityService extendedSecurityService)
{
this.extendedSecurityService = extendedSecurityService;
}
/**
* @param filePlanService file plan service
*/
public void setFilePlanService(FilePlanService filePlanService)
{
this.filePlanService = filePlanService;
}
/**
* @param notificationHelper notification helper
*/
public void setNotificationHelper(RecordsManagementNotificationHelper notificationHelper)
{
this.notificationHelper = notificationHelper;
}
/**
* @param policyComponent policy component
*/
public void setPolicyComponent(PolicyComponent policyComponent)
{
this.policyComponent = policyComponent;
}
/**
* @param ownableService ownable service
*/
public void setOwnableService(OwnableService ownableService)
{
this.ownableService = ownableService;
}
/**
* @param capabilityService capability service
*/
public void setCapabilityService(CapabilityService capabilityService)
{
this.capabilityService = capabilityService;
}
/**
* @param ruleService rule service
*/
public void setRuleService(RuleService ruleService)
{
this.ruleService = ruleService;
}
/**
* @param fileFolderService file folder service
*/
public void setFileFolderService(FileFolderService fileFolderService)
{
this.fileFolderService = fileFolderService;
}
/**
* @param recordFolderService record folder service
*/
public void setRecordFolderService(RecordFolderService recordFolderService)
{
this.recordFolderService = recordFolderService;
}
/**
* @param filePlanRoleService file plan role service
*/
public void setFilePlanRoleService(FilePlanRoleService filePlanRoleService)
{
this.filePlanRoleService = filePlanRoleService;
}
/**
* @param permissionService permission service
*/
public void setPermissionService(PermissionService permissionService)
{
this.permissionService = permissionService;
}
/**
* @param versionService version service
*/
public void setVersionService(VersionService versionService)
{
this.versionService = versionService;
}
/**
* @param relationshipService relationship service
*/
public void setRelationshipService(RelationshipService relationshipService)
{
this.relationshipService = relationshipService;
}
/**
* @param dispositionService disposition service
*/
public void setDispositionService(DispositionService dispositionService)
{
this.dispositionService = dispositionService;
}
/**
* @param recordsManagementContainerType records management container type
*/
public void setRecordsManagementContainerType(RecordsManagementContainerType recordsManagementContainerType)
{
this.recordsManagementContainerType = recordsManagementContainerType;
}
/**
* @param recordableVersionService recordable version service
*/
public void setRecordableVersionService(RecordableVersionService recordableVersionService)
{
this.recordableVersionService = recordableVersionService;
}
/**
* Init method
*/
public void init()
{
// bind policies
beforeFileRecord = policyComponent.registerClassPolicy(BeforeFileRecord.class);
onFileRecord = policyComponent.registerClassPolicy(OnFileRecord.class);
// bind behaviours
policyComponent.bindAssociationBehaviour(
NodeServicePolicies.OnCreateChildAssociationPolicy.QNAME,
TYPE_RECORD_FOLDER,
ContentModel.ASSOC_CONTAINS,
onCreateChildAssociation);
}
/**
* @see org.alfresco.repo.node.NodeServicePolicies.OnRemoveAspectPolicy#onRemoveAspect(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName)
*/
@Override
@Behaviour
(
kind = BehaviourKind.CLASS,
type = "sys:noContent"
)
public void onRemoveAspect(NodeRef nodeRef, QName aspect)
{
if (nodeService.hasAspect(nodeRef, ASPECT_RECORD))
{
ContentData contentData = (ContentData) nodeService.getProperty(nodeRef, ContentModel.PROP_CONTENT);
// Only switch name back to the format of "name (identifierId)" if content size is non-zero, else leave it as the original name to avoid CIFS shuffling issues.
if (contentData != null && contentData.getSize() > 0)
{
switchNames(nodeRef);
}
}
else
{
// check whether filling is pending aspect removal
Set<NodeRef> pendingFilling = transactionalResourceHelper.getSet(KEY_PENDING_FILLING);
if (pendingFilling.contains(nodeRef))
{
file(nodeRef);
}
}
}
/**
* @see org.alfresco.repo.node.NodeServicePolicies.OnAddAspectPolicy#onAddAspect(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName)
*/
@Override
@Behaviour
(
kind = BehaviourKind.CLASS,
type = "sys:noContent"
)
public void onAddAspect(NodeRef nodeRef, QName aspect)
{
switchNames(nodeRef);
}
/**
* Helper method to switch the name of the record around. Used to support record creation via
* file protocols.
*
* @param nodeRef node reference (record)
*/
private void switchNames(NodeRef nodeRef)
{
try
{
if (nodeService.hasAspect(nodeRef, ASPECT_RECORD))
{
String origionalName = (String)nodeService.getProperty(nodeRef, PROP_ORIGIONAL_NAME);
if (origionalName != null)
{
String name = (String)nodeService.getProperty(nodeRef, ContentModel.PROP_NAME);
fileFolderService.rename(nodeRef, origionalName);
nodeService.setProperty(nodeRef, PROP_ORIGIONAL_NAME, name);
}
}
}
catch (FileExistsException e)
{
if (logger.isDebugEnabled())
{
logger.debug(e.getMessage());
}
}
catch (InvalidNodeRefException e)
{
if (logger.isDebugEnabled())
{
logger.debug(e.getMessage());
}
}
catch (FileNotFoundException e)
{
if (logger.isDebugEnabled())
{
logger.debug(e.getMessage());
}
}
}
/**
* Behaviour executed when a new item is added to a record folder.
*
* @see org.alfresco.repo.node.NodeServicePolicies.OnCreateChildAssociationPolicy#onCreateChildAssociation(org.alfresco.service.cmr.repository.ChildAssociationRef, boolean)
*/
@Override
public void onCreateChildAssociation(final ChildAssociationRef childAssocRef, final boolean bNew)
{
AuthenticationUtil.runAs(new RunAsWork<Void>()
{
@Override
public Void doWork()
{
onCreateChildAssociation.disable();
try
{
NodeRef nodeRef = childAssocRef.getChildRef();
if (nodeService.exists(nodeRef) &&
!nodeService.hasAspect(nodeRef, ContentModel.ASPECT_TEMPORARY) &&
!nodeService.getType(nodeRef).equals(TYPE_RECORD_FOLDER) &&
!nodeService.getType(nodeRef).equals(TYPE_RECORD_CATEGORY))
{
if (nodeService.hasAspect(nodeRef, ContentModel.ASPECT_NO_CONTENT))
{
// we need to postpone filling until the NO_CONTENT aspect is removed
Set<NodeRef> pendingFilling = transactionalResourceHelper.getSet(KEY_PENDING_FILLING);
pendingFilling.add(nodeRef);
}
else
{
// store information about the 'new' record in the transaction
// @since 2.3
// @see https://issues.alfresco.com/jira/browse/RM-1956
if (bNew)
{
Set<NodeRef> newRecords = transactionalResourceHelper.getSet(KEY_NEW_RECORDS);
newRecords.add(nodeRef);
}
else
{
// if we are linking a record
NodeRef parentNodeRef = childAssocRef.getParentRef();
if (isRecord(nodeRef) && isRecordFolder(parentNodeRef))
{
// validate the link conditions
validateLinkConditions(nodeRef, parentNodeRef);
}
}
// create and file the content as a record
file(nodeRef);
// recalculate disposition schedule for the record when linking it
dispositionService.recalculateNextDispositionStep(nodeRef);
}
}
}
catch (RecordLinkRuntimeException e)
{
// rethrow exception
throw e;
}
catch (AlfrescoRuntimeException e)
{
// do nothing but log error
if (logger.isWarnEnabled())
{
logger.warn("Unable to file pending record.", e);
}
}
finally
{
onCreateChildAssociation.enable();
}
return null;
}
}, AuthenticationUtil.getSystemUserName());
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.record.RecordService#disablePropertyEditableCheck()
*/
@Override
public void disablePropertyEditableCheck()
{
org.alfresco.repo.policy.Behaviour behaviour = getBehaviour("onUpdateProperties");
if (behaviour != null)
{
getBehaviour("onUpdateProperties").disable();
}
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.record.RecordService#disablePropertyEditableCheck(org.alfresco.service.cmr.repository.NodeRef)
*/
public void disablePropertyEditableCheck(NodeRef nodeRef)
{
Set<NodeRef> ignoreOnUpdate = transactionalResourceHelper.getSet(KEY_IGNORE_ON_UPDATE);
ignoreOnUpdate.add(nodeRef);
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.record.RecordService#enablePropertyEditableCheck()
*/
@Override
public void enablePropertyEditableCheck()
{
org.alfresco.repo.policy.Behaviour behaviour = getBehaviour("onUpdateProperties");
if (behaviour != null)
{
behaviour.enable();
}
}
/**
* Ensure that the user only updates record properties that they have permission to.
*
* @see org.alfresco.repo.node.NodeServicePolicies.OnUpdatePropertiesPolicy#onUpdateProperties(org.alfresco.service.cmr.repository.NodeRef, java.util.Map, java.util.Map)
*/
@Override
@Behaviour
(
name = "onUpdateProperties",
kind = BehaviourKind.CLASS,
type= "rma:record"
)
public void onUpdateProperties(final NodeRef nodeRef, final Map<QName, Serializable> before, final Map<QName, Serializable> after)
{
if (AuthenticationUtil.getFullyAuthenticatedUser() != null &&
!AuthenticationUtil.isRunAsUserTheSystemUser() &&
nodeService.exists(nodeRef) &&
isRecord(nodeRef) &&
!transactionalResourceHelper.getSet(KEY_IGNORE_ON_UPDATE).contains(nodeRef))
{
for (Map.Entry<QName, Serializable> entry : after.entrySet())
{
Serializable beforeValue = null;
QName property = entry.getKey();
if (before != null)
{
beforeValue = before.get(property);
}
Serializable afterValue = entry.getValue();
boolean propertyUnchanged = false;
if (beforeValue instanceof Date && afterValue instanceof Date)
{
// deal with date values, remove the seconds and milliseconds for the
// comparison as they are removed from the submitted for data
Calendar beforeCal = Calendar.getInstance();
beforeCal.setTime((Date)beforeValue);
Calendar afterCal = Calendar.getInstance();
afterCal.setTime((Date)afterValue);
beforeCal.set(Calendar.SECOND, 0);
beforeCal.set(Calendar.MILLISECOND, 0);
afterCal.set(Calendar.SECOND, 0);
afterCal.set(Calendar.MILLISECOND, 0);
propertyUnchanged = (beforeCal.compareTo(afterCal) == 0);
}
else if ((afterValue instanceof Boolean) && (beforeValue == null) && (afterValue.equals(Boolean.FALSE)))
{
propertyUnchanged = true;
}
else
{
// otherwise
propertyUnchanged = EqualsHelper.nullSafeEquals(beforeValue, afterValue);
}
if (!propertyUnchanged &&
!(ContentModel.PROP_CONTENT.equals(property) && beforeValue == null) &&
!isPropertyEditable(nodeRef, property))
{
// the user can't edit the record property
throw new ModelAccessDeniedException(
"The user " + AuthenticationUtil.getFullyAuthenticatedUser() +
" does not have the permission to edit the record property " + property.toString() +
" on the node " + nodeRef.toString());
}
}
}
}
/**
* Get map containing record metadata aspects.
*
* @return {@link Map}<{@link QName}, {@link Set}<{@link QName}>> map containing record metadata aspects
*
* @since 2.2
*/
protected Map<QName, Set<QName>> getRecordMetadataAspectsMap()
{
if (recordMetaDataAspects == null)
{
// create map
recordMetaDataAspects = new HashMap<QName, Set<QName>>();
// init with legacy aspects
initRecordMetaDataMap();
}
return recordMetaDataAspects;
}
/**
* Initialises the record meta-data map.
* <p>
* This is here to support backwards compatibility in case an existing
* customization (pre 2.2) is still using the record meta-data aspect.
*
* @since 2.2
*/
private void initRecordMetaDataMap()
{
// populate the inital set of record meta-data aspects .. this is here for legacy reasons
Collection<QName> 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))
{
recordMetaDataAspects.put(aspect, Collections.singleton(TYPE_FILE_PLAN));
}
}
}
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.record.RecordService#registerRecordMetadataAspect(org.alfresco.service.namespace.QName, org.alfresco.service.namespace.QName)
*/
@Override
public void registerRecordMetadataAspect(QName recordMetadataAspect, QName filePlanType)
{
ParameterCheck.mandatory("recordMetadataAspect", recordMetadataAspect);
ParameterCheck.mandatory("filePlanType", filePlanType);
Set<QName> filePlanTypes = null;
if (getRecordMetadataAspectsMap().containsKey(recordMetadataAspect))
{
// get the current set of file plan types for this aspect
filePlanTypes = getRecordMetadataAspectsMap().get(recordMetadataAspect);
}
else
{
// create a new set for the file plan type
filePlanTypes = new HashSet<QName>(1);
getRecordMetadataAspectsMap().put(recordMetadataAspect, filePlanTypes);
}
// add the file plan type
filePlanTypes.add(filePlanType);
}
/**
* @deprecated since 2.2, file plan is required to provide context
*/
@Override
@Deprecated
public Set<QName> getRecordMetaDataAspects()
{
return getRecordMetadataAspects(TYPE_FILE_PLAN);
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.record.RecordService#isRecordMetadataAspect(org.alfresco.service.namespace.QName)
*/
@Override
public boolean isRecordMetadataAspect(QName aspect)
{
return getRecordMetadataAspectsMap().containsKey(aspect);
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.record.RecordService#isRecordMetadataProperty(org.alfresco.service.namespace.QName)
*/
@Override
public boolean isRecordMetadataProperty(QName property)
{
boolean result = false;
PropertyDefinition propertyDefinition = dictionaryService.getProperty(property);
if (propertyDefinition != null)
{
ClassDefinition classDefinition = propertyDefinition.getContainerClass();
if (classDefinition != null &&
getRecordMetadataAspectsMap().containsKey(classDefinition.getName()))
{
result = true;
}
}
return result;
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.record.RecordService#getRecordMetaDataAspects(org.alfresco.service.cmr.repository.NodeRef)
*/
@Override
public Set<QName> getRecordMetadataAspects(NodeRef nodeRef)
{
QName filePlanType = TYPE_FILE_PLAN;
if (nodeRef != null)
{
NodeRef filePlan = getFilePlan(nodeRef);
filePlanType = nodeService.getType(filePlan);
}
return getRecordMetadataAspects(filePlanType);
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.record.RecordService#getRecordMetadataAspects(org.alfresco.service.namespace.QName)
*/
@Override
public Set<QName> getRecordMetadataAspects(QName filePlanType)
{
Set<QName> result = new HashSet<QName>(getRecordMetadataAspectsMap().size());
for (Entry<QName, Set<QName>> entry : getRecordMetadataAspectsMap().entrySet())
{
if (entry.getValue().contains(filePlanType))
{
result.add(entry.getKey());
}
}
return result;
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.record.RecordService#createRecord(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.cmr.repository.NodeRef)
*/
@Override
public void createRecord(NodeRef filePlan, NodeRef nodeRef)
{
createRecord(filePlan, nodeRef, true);
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.record.RecordService#createRecord(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.cmr.repository.NodeRef, boolean)
*/
@Override
public void createRecord(final NodeRef filePlan, final NodeRef nodeRef, final boolean isLinked)
{
ParameterCheck.mandatory("filePlan", filePlan);
ParameterCheck.mandatory("nodeRef", nodeRef);
ParameterCheck.mandatory("isLinked", isLinked);
// first we do a sanity check to ensure that the user has at least write permissions on the document
if (extendedPermissionService.hasPermission(nodeRef, PermissionService.WRITE) != AccessStatus.ALLOWED)
{
throw new AccessDeniedException("Can not create record from document, because the user " +
AuthenticationUtil.getRunAsUser() +
" does not have Write permissions on the doucment " +
nodeRef.toString());
}
// do the work of creating the record as the system user
AuthenticationUtil.runAsSystem(new RunAsWork<Void>()
{
@Override
public Void doWork()
{
if (!nodeService.hasAspect(nodeRef, ASPECT_RECORD))
{
// disable delete rules
ruleService.disableRuleType("outbound");
try
{
// get the new record container for the file plan
NodeRef newRecordContainer = filePlanService.getUnfiledContainer(filePlan);
if (newRecordContainer == null)
{
throw new AlfrescoRuntimeException("Unable to create record, because new record container could not be found.");
}
// get the documents readers and writers
Pair<Set<String>, Set<String>> readersAndWriters = extendedPermissionService.getReadersAndWriters(nodeRef);
// get the current owner
String owner = ownableService.getOwner(nodeRef);
// get the documents primary parent assoc
ChildAssociationRef parentAssoc = nodeService.getPrimaryParent(nodeRef);
// get the latest version record, if there is one
NodeRef latestVersionRecord = getLatestVersionRecord(nodeRef);
behaviourFilter.disableBehaviour();
try
{
// move the document into the file plan
nodeService.moveNode(nodeRef, newRecordContainer, ContentModel.ASSOC_CONTAINS, parentAssoc.getQName());
}
finally
{
behaviourFilter.enableBehaviour();
}
// save the information about the originating details
Map<QName, Serializable> aspectProperties = new HashMap<QName, Serializable>(3);
aspectProperties.put(PROP_RECORD_ORIGINATING_LOCATION, parentAssoc.getParentRef());
aspectProperties.put(PROP_RECORD_ORIGINATING_USER_ID, owner);
aspectProperties.put(PROP_RECORD_ORIGINATING_CREATION_DATE, new Date());
nodeService.addAspect(nodeRef, ASPECT_RECORD_ORIGINATING_DETAILS, aspectProperties);
// make the document a record
makeRecord(nodeRef);
if (latestVersionRecord != null)
{
// indicate that this is the 'final' record version
PropertyMap versionRecordProps = new PropertyMap(2);
versionRecordProps.put(RecordableVersionModel.PROP_VERSION_LABEL, I18NUtil.getMessage(FINAL_VERSION));
versionRecordProps.put(RecordableVersionModel.PROP_VERSION_DESCRIPTION, I18NUtil.getMessage(FINAL_DESCRIPTION));
nodeService.addAspect(nodeRef, RecordableVersionModel.ASPECT_VERSION_RECORD, versionRecordProps);
// link to previous version
relationshipService.addRelationship(CUSTOM_REF_VERSIONS.getLocalName(), nodeRef, latestVersionRecord);
}
if (isLinked)
{
// turn off rules
ruleService.disableRules();
try
{
// maintain the original primary location
nodeService.addChild(parentAssoc.getParentRef(), nodeRef, parentAssoc.getTypeQName(), parentAssoc.getQName());
// set the extended security
extendedSecurityService.set(nodeRef, readersAndWriters);
}
finally
{
ruleService.enableRules();
}
}
}
finally
{
ruleService.enableRuleType("outbound");
}
}
return null;
}
});
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.record.RecordService#createRecordFromCopy(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.cmr.repository.NodeRef)
*/
@Override
public NodeRef createRecordFromCopy(final NodeRef filePlan, final NodeRef nodeRef)
{
return authenticationUtil.runAsSystem(new RunAsWork<NodeRef>()
{
public NodeRef doWork() throws Exception
{
// get the unfiled record folder
final NodeRef unfiledRecordFolder = filePlanService.getUnfiledContainer(filePlan);
// get the documents readers and writers
Pair<Set<String>, Set<String>> readersAndWriters = extendedPermissionService.getReadersAndWriters(nodeRef);
// copy version state and create record
NodeRef record = null;
try
{
List<AssociationRef> originalAssocs = null;
if (nodeService.hasAspect(nodeRef, ContentModel.ASPECT_COPIEDFROM))
{
// take a note of any copyFrom information already on the node
originalAssocs = nodeService.getTargetAssocs(nodeRef, ContentModel.ASSOC_ORIGINAL);
}
recordsManagementContainerType.disable();
try
{
// create a copy of the original state and add it to the unfiled record container
FileInfo recordInfo = fileFolderService.copy(nodeRef, unfiledRecordFolder, null);
record = recordInfo.getNodeRef();
}
finally
{
recordsManagementContainerType.enable();
}
// if versionable, then remove without destroying version history,
// because it is being shared with the originating document
behaviourFilter.disableBehaviour(ContentModel.ASPECT_VERSIONABLE);
try
{
nodeService.removeAspect(record, ContentModel.ASPECT_VERSIONABLE);
}
finally
{
behaviourFilter.enableBehaviour(ContentModel.ASPECT_VERSIONABLE);
}
// make record
makeRecord(record);
// remove added copy assocs
List<AssociationRef> recordAssocs = nodeService.getTargetAssocs(record, ContentModel.ASSOC_ORIGINAL);
for (AssociationRef recordAssoc : recordAssocs)
{
nodeService.removeAssociation(
recordAssoc.getSourceRef(),
recordAssoc.getTargetRef(),
ContentModel.ASSOC_ORIGINAL);
}
// re-add origional assocs or remove aspect
if (originalAssocs == null)
{
nodeService.removeAspect(record, ContentModel.ASPECT_COPIEDFROM);
}
else
{
for (AssociationRef originalAssoc : originalAssocs)
{
nodeService.createAssociation(record, originalAssoc.getTargetRef(), ContentModel.ASSOC_ORIGINAL);
}
}
}
catch (FileNotFoundException e)
{
throw new AlfrescoRuntimeException("Can't create recorded version, because copy fails.", e);
}
// set extended security on record
extendedSecurityService.set(record, readersAndWriters);
return record;
}
});
}
/**
* Helper to get the latest version record for a given document (ie non-record)
*
* @param nodeRef node reference
* @return NodeRef latest version record, null otherwise
*/
private NodeRef getLatestVersionRecord(NodeRef nodeRef)
{
NodeRef versionRecord = null;
recordableVersionService.createSnapshotVersion(nodeRef);
// wire record up to previous record
VersionHistory versionHistory = versionService.getVersionHistory(nodeRef);
if (versionHistory != null)
{
Collection<Version> previousVersions = versionHistory.getAllVersions();
for (Version previousVersion : previousVersions)
{
// look for the associated record
final NodeRef previousRecord = recordableVersionService.getVersionRecord(previousVersion);
if (previousRecord != null)
{
versionRecord = previousRecord;
break;
}
}
}
return versionRecord;
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.record.RecordService#createNewRecord(org.alfresco.service.cmr.repository.NodeRef, java.lang.String, org.alfresco.service.namespace.QName, java.util.Map, org.alfresco.service.cmr.repository.ContentReader)
*/
@Override
public NodeRef createRecordFromContent(NodeRef parent, String name, QName type, Map<QName, Serializable> properties, ContentReader reader)
{
ParameterCheck.mandatory("nodeRef", parent);
ParameterCheck.mandatory("name", name);
NodeRef result = null;
NodeRef destination = parent;
if (isFilePlan(parent))
{
// get the unfiled record container for the file plan
destination = filePlanService.getUnfiledContainer(parent);
if (destination == null)
{
throw new AlfrescoRuntimeException("Unable to create record, because unfiled container could not be found.");
}
}
// if none set the default record type is cm:content
if (type == null)
{
type = ContentModel.TYPE_CONTENT;
}
else if (!dictionaryService.isSubClass(type, ContentModel.TYPE_CONTENT))
{
throw new AlfrescoRuntimeException("Record can only be created from a sub-type of cm:content.");
}
disablePropertyEditableCheck();
try
{
// create the new record
final NodeRef record = fileFolderService.create(destination, name, type).getNodeRef();
// set the properties
if (properties != null)
{
nodeService.addProperties(record, properties);
}
// set the content
if (reader != null)
{
ContentWriter writer = fileFolderService.getWriter(record);
writer.setEncoding(reader.getEncoding());
writer.setMimetype(reader.getMimetype());
writer.putContent(reader);
}
result = authenticationUtil.runAsSystem(new RunAsWork<NodeRef>()
{
public NodeRef doWork() throws Exception
{
// Check if the "record" aspect has been applied already.
// In case of filing a report the created node will be made
// a record within the "onCreateChildAssociation" method if
// a destination for the report has been selected.
if (!nodeService.hasAspect(record, ASPECT_RECORD))
{
// make record
makeRecord(record);
}
return record;
}
});
}
finally
{
enablePropertyEditableCheck();
}
return result;
}
/**
* Creates a record from the given document
*
* @param document the document from which a record will be created
*/
@Override
public void makeRecord(NodeRef document)
{
ParameterCheck.mandatory("document", document);
ruleService.disableRules();
disablePropertyEditableCheck();
try
{
// get the record id
String recordId = identifierService.generateIdentifier(ASPECT_RECORD,
nodeService.getPrimaryParent(document).getParentRef());
// get the record name
String name = (String)nodeService.getProperty(document, ContentModel.PROP_NAME);
// rename the record
int dotIndex = name.lastIndexOf('.');
String prefix = name;
String postfix = "";
if (dotIndex != -1)
{
prefix = name.substring(0, dotIndex);
postfix = name.substring(dotIndex);
}
String recordName = prefix + " (" + recordId + ")" + postfix;
behaviourFilter.disableBehaviour();
try
{
fileFolderService.rename(document, recordName);
}
finally
{
behaviourFilter.enableBehaviour();
}
if (logger.isDebugEnabled())
{
logger.debug("Rename " + name + " to " + recordName);
}
// add the record aspect
Map<QName, Serializable> props = new HashMap<QName, Serializable>(2);
props.put(PROP_IDENTIFIER, recordId);
props.put(PROP_ORIGIONAL_NAME, name);
nodeService.addAspect(document, RecordsManagementModel.ASPECT_RECORD, props);
// remove versionable aspect(s)
nodeService.removeAspect(document, RecordableVersionModel.ASPECT_VERSIONABLE);
// remove the owner
ownableService.setOwner(document, OwnableService.NO_OWNER);
}
catch (FileNotFoundException e)
{
throw new AlfrescoRuntimeException("Unable to make record, because rename failed.", e);
}
finally
{
ruleService.enableRules();
enablePropertyEditableCheck();
}
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.disposableitem.RecordService#isFiled(org.alfresco.service.cmr.repository.NodeRef)
*/
@Override
public boolean isFiled(final NodeRef nodeRef)
{
ParameterCheck.mandatory("nodeRef", nodeRef);
boolean result = false;
if (isRecord(nodeRef))
{
result = AuthenticationUtil.runAsSystem(new RunAsWork<Boolean>()
{
public Boolean doWork() throws Exception
{
return (null != nodeService.getProperty(nodeRef, PROP_DATE_FILED));
}
});
}
return result;
}
/**
* Helper method to 'file' a new document that arrived in the file plan structure.
*
* TODO atm we only 'file' content as a record .. may need to consider other types if we
* are to support the notion of composite records.
*
* @param record node reference to record (or soon to be record!)
*/
@Override
public void file(NodeRef record)
{
ParameterCheck.mandatory("item", record);
// we only support filling of content items
// TODO composite record support needs to file containers too
QName type = nodeService.getType(record);
if (ContentModel.TYPE_CONTENT.equals(type) ||
dictionaryService.isSubClass(type, ContentModel.TYPE_CONTENT))
{
// fire before file record policy
beforeFileRecord.get(getTypeAndApsects(record)).beforeFileRecord(record);
// check whether this item is already an item or not
if (!isRecord(record))
{
// make the item a record
makeRecord(record);
}
// set filed date
if (nodeService.getProperty(record, PROP_DATE_FILED) == null)
{
Calendar fileCalendar = Calendar.getInstance();
nodeService.setProperty(record, PROP_DATE_FILED, fileCalendar.getTime());
}
// file on file record policy
onFileRecord.get(getTypeAndApsects(record)).onFileRecord(record);
}
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.record.RecordService#rejectRecord(org.alfresco.service.cmr.repository.NodeRef, java.lang.String)
*/
@Override
public void rejectRecord(final NodeRef nodeRef, final String reason)
{
ParameterCheck.mandatory("NodeRef", nodeRef);
ParameterCheck.mandatoryString("Reason", reason);
// Save the id of the currently logged in user
final String userId = AuthenticationUtil.getFullyAuthenticatedUser();
// do the work of rejecting the record as the system user
AuthenticationUtil.runAsSystem(new RunAsWork<Void>()
{
@Override
public Void doWork() throws Exception
{
ruleService.disableRules();
try
{
// get the latest version record, if there is one
NodeRef latestVersionRecord = getLatestVersionRecord(nodeRef);
if (latestVersionRecord != null)
{
relationshipService.removeRelationship(CUSTOM_REF_VERSIONS.getLocalName(), nodeRef, latestVersionRecord);
}
// get record property values
final Map<QName, Serializable> properties = nodeService.getProperties(nodeRef);
final String recordId = (String)properties.get(PROP_IDENTIFIER);
final String documentOwner = (String)properties.get(PROP_RECORD_ORIGINATING_USER_ID);
final String originalName = (String)properties.get(PROP_ORIGIONAL_NAME);
final NodeRef originatingLocation = (NodeRef)properties.get(PROP_RECORD_ORIGINATING_LOCATION);
// we can only reject if the originating location is present
if (originatingLocation != null)
{
// first remove the secondary link association
final List<ChildAssociationRef> parentAssocs = nodeService.getParentAssocs(nodeRef);
for (ChildAssociationRef childAssociationRef : parentAssocs)
{
if (!childAssociationRef.isPrimary() && childAssociationRef.getParentRef().equals(originatingLocation))
{
nodeService.removeChildAssociation(childAssociationRef);
break;
}
}
removeRmAspectsFrom(nodeRef);
// get the records primary parent association
final ChildAssociationRef parentAssoc = nodeService.getPrimaryParent(nodeRef);
// move the record into the collaboration site
nodeService.moveNode(nodeRef, originatingLocation, ContentModel.ASSOC_CONTAINS, parentAssoc.getQName());
// rename to the original name
if (originalName != null)
{
fileFolderService.rename(nodeRef, originalName);
if (logger.isDebugEnabled())
{
String name = (String)nodeService.getProperty(nodeRef, ContentModel.PROP_NAME);
logger.debug("Rename " + name + " to " + originalName);
}
}
// save the information about the rejection details
final Map<QName, Serializable> aspectProperties = new HashMap<>(3);
aspectProperties.put(PROP_RECORD_REJECTION_USER_ID, userId);
aspectProperties.put(PROP_RECORD_REJECTION_DATE, new Date());
aspectProperties.put(PROP_RECORD_REJECTION_REASON, reason);
nodeService.addAspect(nodeRef, ASPECT_RECORD_REJECTION_DETAILS, aspectProperties);
// Restore the owner of the document
if (StringUtils.isBlank(documentOwner))
{
throw new AlfrescoRuntimeException("Unable to find the creator of document.");
}
ownableService.setOwner(nodeRef, documentOwner);
// clear the existing permissions
permissionService.clearPermission(nodeRef, null);
// restore permission inheritance
permissionService.setInheritParentPermissions(nodeRef, true);
// send an email to the record creator
notificationHelper.recordRejectedEmailNotification(nodeRef, recordId, documentOwner);
}
}
finally
{
ruleService.enableRules();
}
return null;
}
/** Removes all RM related aspects from the specified node and any rendition children. */
private void removeRmAspectsFrom(NodeRef nodeRef)
{
// Note that when folder records are supported, we will need to recursively
// remove aspects from their descendants.
final Set<QName> aspects = nodeService.getAspects(nodeRef);
for (QName aspect : aspects)
{
if (RM_URI.equals(aspect.getNamespaceURI()) ||
RecordableVersionModel.RMV_URI.equals(aspect.getNamespaceURI()))
{
nodeService.removeAspect(nodeRef, aspect);
}
}
for (ChildAssociationRef renditionAssoc : renditionService.getRenditions(nodeRef))
{
final NodeRef renditionNode = renditionAssoc.getChildRef();
// Do not attempt to clean up rendition nodes which are not children of their source node.
final boolean renditionRequiresCleaning = nodeService.exists(renditionNode) &&
renditionAssoc.isPrimary();
if (renditionRequiresCleaning)
{
removeRmAspectsFrom(renditionNode);
}
}
}
});
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.record.RecordService#isPropertyEditable(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName)
*/
@Override
public boolean isPropertyEditable(NodeRef record, QName property)
{
ParameterCheck.mandatory("record", record);
ParameterCheck.mandatory("property", property);
if (!isRecord(record))
{
throw new AlfrescoRuntimeException("Can not check if the property " + property.toString() + " is editable, because node reference is not a record.");
}
NodeRef filePlan = getFilePlan(record);
// DEBUG ...
boolean debugEnabled = logger.isDebugEnabled();
if (debugEnabled)
{
logger.debug("Checking whether property " + property.toString() + " is editable for user " + AuthenticationUtil.getRunAsUser());
Set<Role> roles = filePlanRoleService.getRolesByUser(filePlan, AuthenticationUtil.getRunAsUser());
logger.debug(" ... users roles");
for (Role role : roles)
{
logger.debug(" ... user has role " + role.getName() + " with capabilities ");
for (Capability cap : role.getCapabilities())
{
logger.debug(" ... " + cap.getName());
}
}
logger.debug(" ... user has the following set permissions on the file plan");
Set<AccessPermission> perms = permissionService.getAllSetPermissions(filePlan);
for (AccessPermission perm : perms)
{
if ((perm.getPermission().contains(RMPermissionModel.EDIT_NON_RECORD_METADATA) ||
perm.getPermission().contains(RMPermissionModel.EDIT_RECORD_METADATA)))
{
logger.debug(" ... " + perm.getAuthority() + " - " + perm.getPermission() + " - " + perm.getAccessStatus().toString());
}
}
if (permissionService.hasPermission(filePlan, RMPermissionModel.EDIT_NON_RECORD_METADATA).equals(AccessStatus.ALLOWED))
{
logger.debug(" ... user has the edit non record metadata permission on the file plan");
}
}
// END DEBUG ...
boolean result = alwaysEditProperty(property);
if (result)
{
if (debugEnabled)
{
logger.debug(" ... property marked as always editable.");
}
}
else
{
boolean allowRecordEdit = false;
boolean allowNonRecordEdit = false;
AccessStatus accessNonRecord = capabilityService.getCapabilityAccessState(record, RMPermissionModel.EDIT_NON_RECORD_METADATA);
AccessStatus accessDeclaredRecord = capabilityService.getCapabilityAccessState(record, RMPermissionModel.EDIT_DECLARED_RECORD_METADATA);
AccessStatus accessRecord = capabilityService.getCapabilityAccessState(record, RMPermissionModel.EDIT_RECORD_METADATA);
if (AccessStatus.ALLOWED.equals(accessNonRecord))
{
if (debugEnabled)
{
logger.debug(" ... user has edit nonrecord metadata capability");
}
allowNonRecordEdit = true;
}
if (AccessStatus.ALLOWED.equals(accessRecord) ||
AccessStatus.ALLOWED.equals(accessDeclaredRecord))
{
if (debugEnabled)
{
logger.debug(" ... user has edit record or declared metadata capability");
}
allowRecordEdit = true;
}
if (allowNonRecordEdit && allowRecordEdit)
{
if (debugEnabled)
{
logger.debug(" ... so all properties can be edited.");
}
result = true;
}
else if (allowNonRecordEdit && !allowRecordEdit)
{
// can only edit non record properties
if (!isRecordMetadata(filePlan, property))
{
if (debugEnabled)
{
logger.debug(" ... property is not considered record metadata so editable.");
}
result = true;
}
else
{
if (debugEnabled)
{
logger.debug(" ... property is considered record metadata so not editable.");
}
}
}
else if (!allowNonRecordEdit && allowRecordEdit)
{
// can only edit record properties
if (isRecordMetadata(filePlan, property))
{
if (debugEnabled)
{
logger.debug(" ... property is considered record metadata so editable.");
}
result = true;
}
else
{
if (debugEnabled)
{
logger.debug(" ... property is not considered record metadata so not editable.");
}
}
}
// otherwise we can't edit any properties so just return the empty set
}
return result;
}
/**
* Helper method that indicates whether a property is considered record metadata or not.
*
* @param property property
* @return boolea true if record metadata, false otherwise
*/
private boolean isRecordMetadata(NodeRef filePlan, QName property)
{
boolean result = false;
// grab the information about the properties parent type
ClassDefinition parent = null;
PropertyDefinition def = dictionaryService.getProperty(property);
if (def != null)
{
parent = def.getContainerClass();
}
// non-electronic record is considered a special case
// TODO move non-electronic record support to a separate model namespace
if (parent != null && TYPE_NON_ELECTRONIC_DOCUMENT.equals(parent.getName()))
{
result = false;
}
else
{
// check the URI's
result = RECORD_MODEL_URIS.contains(property.getNamespaceURI());
// check the custom model
if (!result && !ArrayUtils.contains(NON_RECORD_MODEL_URIS, property.getNamespaceURI()))
{
if (parent != null && parent.isAspect())
{
result = getRecordMetadataAspects(filePlan).contains(parent.getName());
}
}
}
return result;
}
/**
* Determines whether the property should always be allowed to be edited or not.
*
* @param property
* @return
*/
private boolean alwaysEditProperty(QName property)
{
return (getAlwaysEditURIs().contains(property.getNamespaceURI()) ||
ArrayUtils.contains(ALWAYS_EDIT_PROPERTIES, property) ||
isProtectedProperty(property));
}
/**
* Helper method to determine whether a property is protected at a dictionary definition
* level.
*
* @param property property qualified name
* @return booelan true if protected, false otherwise
*/
private boolean isProtectedProperty(QName property)
{
boolean result = false;
PropertyDefinition def = dictionaryService.getProperty(property);
if (def != null)
{
result = def.isProtected();
}
return result;
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.record.RecordService#isMetadataStub(NodeRef)
*/
@Override
public boolean isMetadataStub(NodeRef nodeRef)
{
ParameterCheck.mandatory("nodeRef", nodeRef);
return nodeService.hasAspect(nodeRef, ASPECT_GHOSTED);
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.record.RecordService#getRecords(NodeRef)
*/
@Override
public List<NodeRef> getRecords(NodeRef recordFolder)
{
ParameterCheck.mandatory("recordFolder", recordFolder);
List<NodeRef> result = new ArrayList<NodeRef>(1);
if (recordFolderService.isRecordFolder(recordFolder))
{
List<ChildAssociationRef> assocs = nodeService.getChildAssocs(recordFolder, ContentModel.ASSOC_CONTAINS, RegexQNamePattern.MATCH_ALL);
for (ChildAssociationRef assoc : assocs)
{
NodeRef child = assoc.getChildRef();
if (isRecord(child))
{
result.add(child);
}
}
}
return result;
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.record.RecordService#addRecordType(NodeRef, QName)
*/
@Override
public void addRecordType(NodeRef nodeRef, QName typeQName)
{
ParameterCheck.mandatory("nodeRef", nodeRef);
ParameterCheck.mandatory("typeQName", typeQName);
if (!nodeService.hasAspect(nodeRef, typeQName))
{
nodeService.addAspect(nodeRef, typeQName, null);
}
else
{
logger.info(I18NUtil.getMessage(MSG_NODE_HAS_ASPECT, nodeRef.toString(), typeQName.toString()));
}
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.record.RecordService#link(NodeRef, NodeRef)
*/
@Override
public void link(NodeRef record, NodeRef recordFolder)
{
ParameterCheck.mandatory("record", record);
ParameterCheck.mandatory("recordFolder", recordFolder);
// ensure we are linking a record to a record folder
if(isRecord(record) && isRecordFolder(recordFolder))
{
// ensure that we are not linking a record to an existing location
List<ChildAssociationRef> parents = nodeService.getParentAssocs(record);
for (ChildAssociationRef parent : parents)
{
if (parent.getParentRef().equals(recordFolder))
{
// we can not link a record to the same location more than once
throw new RecordLinkRuntimeException("Can not link a record to the same record folder more than once");
}
}
// validate link conditions
validateLinkConditions(record, recordFolder);
// get the current name of the record
String name = nodeService.getProperty(record, ContentModel.PROP_NAME).toString();
// create a secondary link to the record folder
nodeService.addChild(
recordFolder,
record,
ContentModel.ASSOC_CONTAINS,
QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, name));
// recalculate disposition schedule for the record when linking it
dispositionService.recalculateNextDispositionStep(record);
}
else
{
// can only link a record to a record folder
throw new RecordLinkRuntimeException("Can only link a record to a record folder.");
}
}
/**
*
* @param record
* @param recordFolder
*/
private void validateLinkConditions(NodeRef record, NodeRef recordFolder)
{
// ensure that the linking record folders have compatible disposition schedules
// 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);
if (recordFolderDispositionSchedule != null)
{
if (recordDispositionSchedule.isRecordLevelDisposition() != recordFolderDispositionSchedule.isRecordLevelDisposition())
{
// we can't link a record to an incompatible disposition schedule
throw new RecordLinkRuntimeException("Can not link a record to a record folder with an incompatible retention schedule. "
+ "They must either both be record level or record folder level retentions.");
}
}
}
}
/**
* @see org.alfresco.module.org_alfresco_module_rm.record.RecordService#unlink(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.cmr.repository.NodeRef)
*/
@Override
public void unlink(NodeRef record, NodeRef recordFolder)
{
ParameterCheck.mandatory("record", record);
ParameterCheck.mandatory("recordFolder", recordFolder);
// ensure we are unlinking a record from a record folder
if(isRecord(record) && isRecordFolder(recordFolder))
{
// check that we are not trying to unlink the primary parent
NodeRef primaryParent = nodeService.getPrimaryParent(record).getParentRef();
if (primaryParent.equals(recordFolder))
{
throw new RecordLinkRuntimeException("Can't unlink a record from it's owning record folder.");
}
// remove the link
nodeService.removeChild(recordFolder, record);
// recalculate disposition schedule for record after unlinking it
dispositionService.recalculateNextDispositionStep(record);
}
else
{
// can only unlink a record from a record folder
throw new RecordLinkRuntimeException("Can only unlink a record from a record folder.");
}
}
}