/* * #%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.action.impl; import java.util.Arrays; import java.util.List; import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.model.ContentModel; import org.alfresco.module.org_alfresco_module_rm.action.RMActionExecuterAbstractBase; import org.alfresco.module.org_alfresco_module_rm.fileplan.FilePlanService; import org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel; import org.alfresco.repo.action.ParameterDefinitionImpl; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; import org.alfresco.repo.transaction.RetryingTransactionHelper; import org.alfresco.service.cmr.action.Action; import org.alfresco.service.cmr.action.ParameterDefinition; import org.alfresco.service.cmr.dictionary.DataTypeDefinition; import org.alfresco.service.cmr.model.FileFolderService; import org.alfresco.service.cmr.model.FileNotFoundException; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.namespace.QName; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.util.StringUtils; /** * File To action implementation. * * @author Mark Hibbins * @since 2.2 */ public abstract class CopyMoveLinkFileToBaseAction extends RMActionExecuterAbstractBase { private static Log logger = LogFactory.getLog(CopyMoveLinkFileToBaseAction.class); /** action parameters */ public static final String PARAM_DESTINATION_RECORD_FOLDER = "destinationRecordFolder"; public static final String PARAM_PATH = "path"; public static final String PARAM_CREATE_RECORD_PATH = "createRecordPath"; public static final String ACTION_FILETO = "fileTo"; public static final String ACTION_LINKTO = "linkTo"; /** file folder service */ private FileFolderService fileFolderService; /** file plan service */ private FilePlanService filePlanService; /** action modes */ public enum CopyMoveLinkFileToActionMode { COPY, MOVE, LINK }; /** Action Mode */ private CopyMoveLinkFileToActionMode mode; /** * @return Action Mode */ protected CopyMoveLinkFileToActionMode getMode() { return this.mode; } /** * Sets the action mode * * @param mode Action mode */ protected void setMode(CopyMoveLinkFileToActionMode mode) { this.mode = mode; } /** * @param fileFolderService file folder service */ public void setFileFolderService(FileFolderService fileFolderService) { this.fileFolderService = fileFolderService; } /** * @param filePlanService file plan service */ public void setFilePlanService(FilePlanService filePlanService) { this.filePlanService = filePlanService; } /** * @see org.alfresco.module.org_alfresco_module_rm.action.RMActionExecuterAbstractBase#addParameterDefinitions(java.util.List) */ @Override protected void addParameterDefinitions(List<ParameterDefinition> paramList) { paramList.add(new ParameterDefinitionImpl(PARAM_PATH, DataTypeDefinition.TEXT, false, getParamDisplayLabel(PARAM_PATH))); paramList.add(new ParameterDefinitionImpl(PARAM_CREATE_RECORD_PATH, DataTypeDefinition.BOOLEAN, false, getParamDisplayLabel(PARAM_CREATE_RECORD_PATH))); } /** * @see org.alfresco.repo.action.executer.ActionExecuterAbstractBase#executeImpl(org.alfresco.service.cmr.action.Action, org.alfresco.service.cmr.repository.NodeRef) */ @Override protected synchronized void executeImpl(final Action action, final NodeRef actionedUponNodeRef) { String actionName = action.getActionDefinitionName(); if (isOkToProceedWithAction(actionedUponNodeRef, actionName)) { QName actionedUponType = getNodeService().getType(actionedUponNodeRef); boolean targetIsUnfiledRecords; if (ACTION_FILETO.equals(action.getActionDefinitionName())) { targetIsUnfiledRecords = false; } else { targetIsUnfiledRecords = (getDictionaryService().isSubClass(actionedUponType, ContentModel.TYPE_CONTENT) && !getRecordService().isFiled(actionedUponNodeRef)) || TYPE_UNFILED_RECORD_FOLDER.equals(actionedUponType); } // first look to see if the destination record folder has been specified NodeRef recordFolder = (NodeRef)action.getParameterValue(PARAM_DESTINATION_RECORD_FOLDER); if (recordFolder == null) { recordFolder = createOrResolvePath(action, actionedUponNodeRef, targetIsUnfiledRecords); } // now we have the reference to the target folder we can do some final checks to see if the action is valid validateActionPostPathResolution(actionedUponNodeRef, recordFolder, actionName, targetIsUnfiledRecords); final NodeRef finalRecordFolder = recordFolder; AuthenticationUtil.runAsSystem(new RunAsWork<Void>() { @Override public Void doWork() { try { if (getMode() == CopyMoveLinkFileToActionMode.MOVE) { fileFolderService.move(actionedUponNodeRef, finalRecordFolder, null); } else if (getMode() == CopyMoveLinkFileToActionMode.COPY) { fileFolderService.copy(actionedUponNodeRef, finalRecordFolder, null); } else if (getMode() == CopyMoveLinkFileToActionMode.LINK) { getRecordService().link(actionedUponNodeRef, finalRecordFolder); } } catch (FileNotFoundException fileNotFound) { throw new AlfrescoRuntimeException("Unable to execute file to action, because the " + (mode == CopyMoveLinkFileToActionMode.MOVE ? "move" : "copy") + " operation failed.", fileNotFound); } return null; } }); } } /** * Return true if the passed parameters to the action are valid for the given action * * @param actionedUponNodeRef * @param actionName * @return */ private boolean isOkToProceedWithAction(NodeRef actionedUponNodeRef, String actionName) { // Check that the incoming parameters are valid prior to performing any action boolean okToProceed = false; if(getNodeService().exists(actionedUponNodeRef) && !getFreezeService().isFrozen(actionedUponNodeRef)) { QName actionedUponType = getNodeService().getType(actionedUponNodeRef); if(ACTION_FILETO.equals(actionName)) { // file to action can only be performed on unfiled records okToProceed = !getRecordService().isFiled(actionedUponNodeRef) && getDictionaryService().isSubClass(actionedUponType, ContentModel.TYPE_CONTENT); if(!okToProceed && logger.isDebugEnabled()) { logger.debug("Unable to run " + actionName + " action on a node that isn't unfiled and a sub-class of content type"); } } else if(ACTION_LINKTO.equals(actionName)) { // link to action can only be performed on filed records okToProceed = getRecordService().isFiled(actionedUponNodeRef) && getDictionaryService().isSubClass(actionedUponType, ContentModel.TYPE_CONTENT); if(!okToProceed && logger.isDebugEnabled()) { logger.debug("Unable to run " + actionName + " action on a node that isn't filed and a sub-class of content type"); } } else { okToProceed = true; } } return okToProceed; } /** * Do a final validation for the parameters and the resolve target path * * @param actionedUponNodeRef * @param target * @param actionName * @param targetIsUnfiledRecords */ private void validateActionPostPathResolution(NodeRef actionedUponNodeRef, NodeRef target, String actionName, boolean targetIsUnfiledRecords) { QName actionedUponType = getNodeService().getType(actionedUponNodeRef); // now we have the reference to the target folder we can do some final checks to see if the action is valid if (target == null) { throw new AlfrescoRuntimeException("Unable to run " + actionName + " action, because the destination record folder could not be determined."); } if(targetIsUnfiledRecords) { QName targetFolderType = getNodeService().getType(target); if(!TYPE_UNFILED_RECORD_CONTAINER.equals(targetFolderType) && !TYPE_UNFILED_RECORD_FOLDER.equals(targetFolderType)) { throw new AlfrescoRuntimeException("Unable to run " + actionName + " action, because the destination record folder is an inappropriate type."); } } else { if(getRecordFolderService().isRecordFolder(target) && !getDictionaryService().isSubClass(actionedUponType, ContentModel.TYPE_CONTENT) && (getRecordFolderService().isRecordFolder(actionedUponNodeRef) || filePlanService.isRecordCategory(actionedUponNodeRef))) { throw new AlfrescoRuntimeException("Unable to run " + actionName + " action, because the destination record folder is an inappropriate type. A record folder cannot contain another folder or a category"); } else if(filePlanService.isRecordCategory(target) && getDictionaryService().isSubClass(actionedUponType, ContentModel.TYPE_CONTENT)) { throw new AlfrescoRuntimeException("Unable to run " + actionName + " action, because the destination record folder is an inappropriate type. A record category cannot contain a record"); } } } /** * Create or resolve the path specified in the action's path parameter * * @param action * @param actionedUponNodeRef * @param targetisUnfiledRecords true is the target is in unfiled records * @return */ private NodeRef createOrResolvePath(final Action action, final NodeRef actionedUponNodeRef, final boolean targetisUnfiledRecords) { // get the starting context final NodeRef context = getContext(action, actionedUponNodeRef, targetisUnfiledRecords); NodeRef path = context; // get the path we wish to resolve String pathParameter = (String)action.getParameterValue(PARAM_PATH); final String[] pathElementsArray = StringUtils.tokenizeToStringArray(pathParameter, "/", false, true); if((pathElementsArray != null) && (pathElementsArray.length > 0)) { // get the create parameter Boolean createValue = (Boolean)action.getParameterValue(PARAM_CREATE_RECORD_PATH); final boolean create = createValue == null ? false : createValue.booleanValue(); // create or resolve the specified path path = getTransactionService().getRetryingTransactionHelper().doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<NodeRef>() { public NodeRef execute() throws Throwable { return createOrResolvePath(action, context, actionedUponNodeRef, Arrays.asList(pathElementsArray), targetisUnfiledRecords, create, false); } }, false, true); } return path; } /** * Create or resolve the specified path * * @param action Action to use for reporting if anything goes wrong * @param parent Parent of path to be created * @param actionedUponNodeRef The node subject to the file/move/copy action * @param pathElements The elements of the path to be created * @param targetisUnfiledRecords true if the target is within unfiled records * @param create true if the path should be creeated if it does not exist * @param creating true if we have already created the parent and therefore can skip the check to see if the next path element already exists * @return */ private NodeRef createOrResolvePath(Action action, NodeRef parent, NodeRef actionedUponNodeRef, List<String> pathElements, boolean targetisUnfiledRecords, boolean create, boolean creating) { NodeRef nodeRef = null; String childName = pathElements.get(0); boolean lastPathElement = pathElements.size() == 1; if(!creating) { nodeRef = getChild(parent, childName); } if(nodeRef == null) { if(create) { creating = true; boolean lastAsFolder = lastPathElement && (getDictionaryService().isSubClass(getNodeService().getType(actionedUponNodeRef), ContentModel.TYPE_CONTENT) || RecordsManagementModel.TYPE_NON_ELECTRONIC_DOCUMENT.equals(getNodeService().getType(actionedUponNodeRef))); nodeRef = createChild(action, parent, childName, targetisUnfiledRecords, lastAsFolder); } else { throw new AlfrescoRuntimeException("Unable to execute " + action.getActionDefinitionName() + " action, because the destination path could not be determined."); } } else { QName nodeType = getNodeService().getType(nodeRef); if(nodeType.equals(RecordsManagementModel.TYPE_HOLD_CONTAINER) || nodeType.equals(RecordsManagementModel.TYPE_TRANSFER_CONTAINER) || nodeType.equals(RecordsManagementModel.TYPE_UNFILED_RECORD_CONTAINER)) { throw new AlfrescoRuntimeException("Unable to execute " + action.getActionDefinitionName() + " action, because the destination path in invalid."); } } if(pathElements.size() > 1) { nodeRef = createOrResolvePath(action, nodeRef, actionedUponNodeRef, pathElements.subList(1, pathElements.size()), targetisUnfiledRecords, create, creating); } return nodeRef; } /** * Get the specified child node ref of the specified parent if it exists, otherwise return null * * @param parent * @param childName * @return */ private NodeRef getChild(NodeRef parent, String childName) { return getNodeService().getChildByName(parent, ContentModel.ASSOC_CONTAINS, childName); } /** * Create the specified child of the specified parent * * @param action Action to use for reporting if anything goes wrong * @param parent Parent of the child to be created * @param childName The name of the child to be created * @param targetisUnfiledRecords true if the child is being created in the unfiled directory (determines type as unfiled container child) * @param lastAsFolder true if this is the last element of the pathe being created and it should be created as a folder. ignored if targetIsUnfiledRecords is true * @return */ private NodeRef createChild(final Action action, final NodeRef parent, final String childName, final boolean targetisUnfiledRecords, final boolean lastAsFolder) { return AuthenticationUtil.runAsSystem(new RunAsWork<NodeRef>() { @Override public NodeRef doWork() { // double check that the child hasn't been created by another thread NodeRef child = getChild(parent, childName); if (child == null) { if (targetisUnfiledRecords) { // create unfiled folder child = fileFolderService.create(parent, childName, RecordsManagementModel.TYPE_UNFILED_RECORD_FOLDER).getNodeRef(); } else if(lastAsFolder) { // create record folder child = getRecordFolderService().createRecordFolder(parent, childName); } else { // ensure we are not trying to create a record categtory in a record folder if(RecordsManagementModel.TYPE_RECORD_FOLDER.equals(getNodeService().getType(parent))) { throw new AlfrescoRuntimeException("Unable to execute " + action.getActionDefinitionName() + " action, because the destination path has a record category within a record folder."); } // create record category child = filePlanService.createRecordCategory(parent, childName); } } return child; } }); } /** * Return the context. This will be the unfiled records container of the context if targetisUnfiledRecords is true * * @param action * @param actionedUponNodeRef * @param targetisUnfiledRecords * @return */ private NodeRef getContext(Action action, NodeRef actionedUponNodeRef, boolean targetisUnfiledRecords) { NodeRef context = filePlanService.getFilePlan(actionedUponNodeRef); if(targetisUnfiledRecords && (context != null) && getNodeService().exists(context)) { context = filePlanService.getUnfiledContainer(context); } if((context == null) || (!getNodeService().exists(context))) { throw new AlfrescoRuntimeException("Unable to execute " + action.getActionDefinitionName() + " action, because the path resolution context could not be determined."); } return context; } }