org.alfresco.rm.rest.api.impl.FilePlanComponentsApiUtils.java Source code

Java tutorial

Introduction

Here is the source code for org.alfresco.rm.rest.api.impl.FilePlanComponentsApiUtils.java

Source

/*
 * #%L
 * Alfresco Records Management Module
 * %%
 * Copyright (C) 2005 - 2017 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.rm.rest.api.impl;

import static org.alfresco.module.org_alfresco_module_rm.util.RMParameterCheck.checkNotBlank;
import static org.alfresco.util.ParameterCheck.mandatory;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;

import org.alfresco.model.ContentModel;
import org.alfresco.module.org_alfresco_module_rm.capability.CapabilityService;
import org.alfresco.module.org_alfresco_module_rm.fileplan.FilePlanService;
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.util.AuthenticationUtil;
import org.alfresco.repo.content.ContentLimitViolationException;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.repo.model.filefolder.FileFolderServiceImpl.InvalidTypeException;
import org.alfresco.repo.node.getchildren.FilterProp;
import org.alfresco.repo.node.getchildren.FilterPropBoolean;
import org.alfresco.repo.node.getchildren.GetChildrenCannedQuery;
import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
import org.alfresco.repo.tenant.TenantUtil;
import org.alfresco.rest.antlr.WhereClauseParser;
import org.alfresco.rest.api.Activities;
import org.alfresco.rest.api.Nodes;
import org.alfresco.rest.api.model.PathInfo;
import org.alfresco.rest.api.model.PathInfo.ElementInfo;
import org.alfresco.rest.framework.core.exceptions.ConstraintViolatedException;
import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException;
import org.alfresco.rest.framework.core.exceptions.InsufficientStorageException;
import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException;
import org.alfresco.rest.framework.core.exceptions.NotFoundException;
import org.alfresco.rest.framework.core.exceptions.RequestEntityTooLargeException;
import org.alfresco.rest.framework.resource.content.BinaryResource;
import org.alfresco.rest.framework.resource.parameters.Parameters;
import org.alfresco.rest.framework.resource.parameters.where.Query;
import org.alfresco.rest.framework.resource.parameters.where.QueryHelper;
import org.alfresco.rest.workflow.api.impl.MapBasedQueryWalker;
import org.alfresco.rm.rest.api.RMSites;
import org.alfresco.rm.rest.api.model.RMNode;
import org.alfresco.rm.rest.api.model.RMSite;
import org.alfresco.rm.rest.api.model.TransferContainer;
import org.alfresco.service.cmr.activities.ActivityInfo;
import org.alfresco.service.cmr.activities.ActivityPoster;
import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.lock.NodeLockedException;
import org.alfresco.service.cmr.model.FileFolderService;
import org.alfresco.service.cmr.model.FileInfo;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.cmr.repository.DuplicateChildNodeNameException;
import org.alfresco.service.cmr.repository.MimetypeService;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.Path;
import org.alfresco.service.cmr.repository.Path.Element;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.security.AccessStatus;
import org.alfresco.service.cmr.security.PermissionService;
import org.alfresco.service.cmr.usage.ContentQuotaException;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.Pair;
import org.alfresco.util.ParameterCheck;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.social.InternalServerErrorException;

import net.sf.acegisecurity.vote.AccessDecisionVoter;

/**
 * Utility class that handles common api endpoint tasks
 *
 * @author Ana Bozianu
 * @since 2.6
 */
public class FilePlanComponentsApiUtils {
    private static final Logger LOGGER = LoggerFactory.getLogger(SearchTypesFactory.class);

    public static final String FILE_PLAN_ALIAS = "-filePlan-";
    public static final String TRANSFERS_ALIAS = "-transfers-";
    public static final String UNFILED_ALIAS = "-unfiled-";
    public static final String HOLDS_ALIAS = "-holds-";
    public static final String RM_SITE_ID = "rm";
    //public static String PARAM_RELATIVE_PATH = "relativePath";

    // excluded properties
    public static final List<QName> TYPES_CAN_CREATE = Arrays.asList(RecordsManagementModel.TYPE_FILE_PLAN,
            RecordsManagementModel.TYPE_RECORD_CATEGORY, RecordsManagementModel.TYPE_RECORD_FOLDER,
            RecordsManagementModel.TYPE_UNFILED_RECORD_CONTAINER, RecordsManagementModel.TYPE_UNFILED_RECORD_FOLDER,
            RecordsManagementModel.TYPE_HOLD_CONTAINER);

    /** RM Nodes API */
    private Nodes nodes;
    private FileFolderService fileFolderService;
    private FilePlanService filePlanService;
    private NodeService nodeService;
    private ContentService contentService;
    private MimetypeService mimetypeService;
    private DictionaryService dictionaryService;
    private CapabilityService capabilityService;
    private PermissionService permissionService;
    private RecordService recordService;
    private AuthenticationUtil authenticationUtil;
    private ActivityPoster activityPoster;
    private RMSites sites;

    public void setNodes(Nodes nodes) {
        this.nodes = nodes;
    }

    public void setFileFolderService(FileFolderService fileFolderService) {
        this.fileFolderService = fileFolderService;
    }

    public void setFilePlanService(FilePlanService filePlanService) {
        this.filePlanService = filePlanService;
    }

    public void setNodeService(NodeService nodeService) {
        this.nodeService = nodeService;
    }

    public void setContentService(ContentService contentService) {
        this.contentService = contentService;
    }

    public void setMimetypeService(MimetypeService mimetypeService) {
        this.mimetypeService = mimetypeService;
    }

    public void setDictionaryService(DictionaryService dictionaryService) {
        this.dictionaryService = dictionaryService;
    }

    public void setCapabilityService(CapabilityService capabilityService) {
        this.capabilityService = capabilityService;
    }

    public void setPermissionService(PermissionService permissionService) {
        this.permissionService = permissionService;
    }

    public void setRecordService(RecordService recordService) {
        this.recordService = recordService;
    }

    public void setAuthenticationUtil(AuthenticationUtil authenticationUtil) {
        this.authenticationUtil = authenticationUtil;
    }

    public void setActivityPoster(ActivityPoster poster) {
        this.activityPoster = poster;
    }

    public void setSites(RMSites sites) {
        this.sites = sites;
    }

    /**
     * lookup node and validate type
     *
     * @param nodeId
     * @param expectedNodeType
     * @return
     * @throws EntityNotFoundException
     */
    public NodeRef lookupAndValidateNodeType(String nodeId, QName expectedNodeType) throws EntityNotFoundException {
        return lookupAndValidateNodeType(nodeId, expectedNodeType, null);
    }

    /**
     * lookup node by id and relative path and validate type
     *
     * @param nodeId
     * @param expectedNodeType
     * @param relativePath
     * @return
     * @throws EntityNotFoundException
     */
    public NodeRef lookupAndValidateNodeType(String nodeId, QName expectedNodeType, String relativePath)
            throws EntityNotFoundException {
        return lookupAndValidateNodeType(nodeId, expectedNodeType, relativePath, false);
    }

    /**
     * lookup node by id and relative path and validate type
     *
     * @param nodeId
     * @param expectedNodeType
     * @param relativePath
     * @return
     * @throws EntityNotFoundException
     */
    public NodeRef lookupAndValidateNodeType(String nodeId, QName expectedNodeType, String relativePath,
            boolean readOnlyRelativePath) throws EntityNotFoundException {
        ParameterCheck.mandatoryString("nodeId", nodeId);
        ParameterCheck.mandatory("expectedNodeType", expectedNodeType);

        /*
         * Lookup by placeholder
         */
        NodeRef nodeRef;
        if (nodeId.equals(FILE_PLAN_ALIAS)) {
            NodeRef filePlan = filePlanService.getFilePlanBySiteId(FilePlanService.DEFAULT_RM_SITE_ID);
            if (filePlan != null) {
                nodeRef = filePlan;
            } else {
                throw new EntityNotFoundException(nodeId);
            }
        } else if (nodeId.equals(TRANSFERS_ALIAS)) {
            NodeRef filePlan = filePlanService.getFilePlanBySiteId(FilePlanService.DEFAULT_RM_SITE_ID);
            if (filePlan != null) {
                nodeRef = filePlanService.getTransferContainer(filePlan);
            } else {
                throw new EntityNotFoundException(nodeId);
            }
        } else if (nodeId.equals(UNFILED_ALIAS)) {
            NodeRef filePlan = filePlanService.getFilePlanBySiteId(FilePlanService.DEFAULT_RM_SITE_ID);
            if (filePlan != null) {
                nodeRef = filePlanService.getUnfiledContainer(filePlan);
            } else {
                throw new EntityNotFoundException(nodeId);
            }
        } else if (nodeId.equals(HOLDS_ALIAS)) {
            NodeRef filePlan = filePlanService.getFilePlanBySiteId(FilePlanService.DEFAULT_RM_SITE_ID);
            if (filePlan != null) {
                nodeRef = filePlanService.getHoldContainer(filePlan);
            } else {
                throw new EntityNotFoundException(nodeId);
            }
        } else {
            nodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, nodeId);
        }

        QName nodeType = nodeService.getType(nodeRef);
        if (!nodeType.equals(expectedNodeType)) {
            throw new InvalidArgumentException("The given id:'" + nodeId + "' (nodeType:" + nodeType.toString()
                    + ") is not valid for this endpoint. Expected nodeType is:" + expectedNodeType.toString());
        }

        if (StringUtils.isNotBlank(relativePath)) {
            nodeRef = lookupAndValidateRelativePath(nodeRef, relativePath, readOnlyRelativePath, expectedNodeType);
        }
        return nodeRef;
    }

    /**
     * TODO
     * @param parameters
     * @return
     */
    public List<Pair<QName, Boolean>> getSortProperties(Parameters parameters) {
        List<Pair<QName, Boolean>> sortProps = new ArrayList<>();
        sortProps.add(new Pair<>(GetChildrenCannedQuery.SORT_QNAME_NODE_TYPE, true));
        sortProps.add(new Pair<>(ContentModel.PROP_NAME, true));
        return sortProps;
    }

    /**
     * Write content to file
     *
     * @param nodeRef  the node to write the content to
     * @param fileName  the name of the file (used for guessing the file's mimetype)
     * @param stream  the input stream to write
     * @param guessEncoding  whether to guess stream encoding
     */
    public void writeContent(NodeRef nodeRef, String fileName, InputStream stream, boolean guessEncoding) {
        try {
            ContentWriter writer = contentService.getWriter(nodeRef, ContentModel.PROP_CONTENT, true);

            String mimeType = mimetypeService.guessMimetype(fileName);
            if ((mimeType != null) && (!mimeType.equals(MimetypeMap.MIMETYPE_BINARY))) {
                // quick/weak guess based on file extension
                writer.setMimetype(mimeType);
            } else {
                // stronger guess based on file stream
                writer.guessMimetype(fileName);
            }

            InputStream is = null;

            if (guessEncoding) {
                is = new BufferedInputStream(stream);
                is.mark(1024);
                writer.setEncoding(guessEncoding(is, mimeType, false));
                try {
                    is.reset();
                } catch (IOException ioe) {
                    if (LOGGER.isWarnEnabled()) {
                        LOGGER.warn("Failed to reset stream after trying to guess encoding: " + ioe.getMessage());
                    }
                }
            } else {
                is = stream;
            }

            writer.putContent(is);
        } catch (ContentQuotaException cqe) {
            throw new InsufficientStorageException();
        } catch (ContentLimitViolationException clv) {
            throw new RequestEntityTooLargeException(clv.getMessage());
        } catch (ContentIOException cioe) {
            if (cioe.getCause() instanceof NodeLockedException) {
                throw (NodeLockedException) cioe.getCause();
            }
            throw cioe;
        }
    }

    /**
     * Helper method that guesses the encoding of a stream of data
     * @param in  the stream to guess the encoding for
     * @param mimeType  the mimetype of the file
     * @param close  if true the stream will be closed at the end
     * @return the stream encoding
     */
    private String guessEncoding(InputStream in, String mimeType, boolean close) {
        String encoding = "UTF-8";
        try {
            if (in != null) {
                Charset charset = mimetypeService.getContentCharsetFinder().getCharset(in, mimeType);
                encoding = charset.name();
            }
        } finally {
            try {
                if (close && (in != null)) {
                    in.close();
                }
            } catch (IOException ioe) {
                if (LOGGER.isWarnEnabled()) {
                    LOGGER.warn("Failed to close stream after trying to guess encoding: " + ioe.getMessage());
                }
            }
        }
        return encoding;
    }

    /**
     * Helper method that creates a relative path if it doesn't already exist
     * The relative path will be build with nodes of the type specified in nodesType
     * If the relative path already exists the method validates if the last element is of type nodesType
     * The method does not validate the type of parentNodeRef
     *
     * @param parentNodeRef  the first node of the path
     * @param relativePath  a string representing the relative path in the format "Folder1/Folder2/Folder3"
     * @param nodesType  the type of all the containers in the path
     * @return  the last element of the relative path
     */
    public NodeRef lookupAndValidateRelativePath(final NodeRef parentNodeRef, String relativePath,
            QName nodesType) {
        return lookupAndValidateRelativePath(parentNodeRef, relativePath, false, nodesType);
    }

    /**
     * Helper method that creates a relative path if it doesn't already exist and if relative path is not read only.
     * If relative path is read only an exception will be thrown if the provided relative path does not exist.
     * The relative path will be build with nodes of the type specified in nodesType
     * If the relative path already exists the method validates if the last element is of type nodesType
     * The method does not validate the type of parentNodeRef
     *
     * @param parentNodeRef  the first node of the path
     * @param relativePath  a string representing the relative path in the format "Folder1/Folder2/Folder3"
     * @param readOnlyRelativePath the flag that indicates if the relativePath should be created if doesn't exist or not
     * @param nodesType  the type of all the containers in the path
     * @return  the last element of the relative path
     */
    public NodeRef lookupAndValidateRelativePath(final NodeRef parentNodeRef, String relativePath,
            boolean readOnlyRelativePath, QName nodesType) {
        mandatory("parentNodeRef", parentNodeRef);
        mandatory("nodesType", nodesType);
        if (StringUtils.isBlank(relativePath)) {
            return parentNodeRef;
        }
        List<String> pathElements = getPathElements(relativePath);
        if (pathElements.isEmpty()) {
            return parentNodeRef;
        }

        /*
         * Get the latest existing path element
         */
        NodeRef lastNodeRef = parentNodeRef;
        int i = 0;
        for (; i < pathElements.size(); i++) {
            final String pathElement = pathElements.get(i);
            final NodeRef contextParentNodeRef = lastNodeRef;
            // Navigation should not check permissions
            NodeRef child = authenticationUtil.runAsSystem(new RunAsWork<NodeRef>() {
                @Override
                public NodeRef doWork() throws Exception {
                    return nodeService.getChildByName(contextParentNodeRef, ContentModel.ASSOC_CONTAINS,
                            pathElement);
                }
            });

            if (child == null) {
                break;
            }
            lastNodeRef = child;
        }
        if (i == pathElements.size()) {
            QName nodeType = nodeService.getType(lastNodeRef);
            if (!nodeType.equals(nodesType)) {
                throw new InvalidArgumentException(
                        "The given id:'" + parentNodeRef.getId() + "' and the relative path '" + relativePath
                                + "' reach a node type invalid for this endpoint." + " Expected nodeType is:"
                                + nodesType.toString() + ". Actual nodeType is:" + nodeType);
            }
            return lastNodeRef;
        } else {
            if (!readOnlyRelativePath) {
                pathElements = pathElements.subList(i, pathElements.size());
            } else {
                throw new NotFoundException("The entity with relativePath: " + relativePath + " was not found.");
            }
        }

        /*
         * Starting from the latest existing element create the rest of the elements
         */
        if (nodesType.equals(RecordsManagementModel.TYPE_UNFILED_RECORD_FOLDER)) {
            for (String pathElement : pathElements) {
                lastNodeRef = fileFolderService
                        .create(lastNodeRef, pathElement, RecordsManagementModel.TYPE_UNFILED_RECORD_FOLDER)
                        .getNodeRef();
            }
        } else if (nodesType.equals(RecordsManagementModel.TYPE_RECORD_CATEGORY)) {
            for (String pathElement : pathElements) {
                lastNodeRef = filePlanService.createRecordCategory(lastNodeRef, pathElement);
            }
        } else {
            // Throw internal error as this method should not be called for other types
            throw new InternalServerErrorException(
                    "Creating relative path of type '" + nodesType + "' not suported for this endpoint");
        }

        return lastNodeRef;
    }

    /**
     * Helper method that parses a string representing a file path and returns a list of element names
     * @param path the file path represented as a string
     * @return a list of file path element names
     */
    private List<String> getPathElements(String path) {
        final List<String> pathElements = new ArrayList<>();
        if (path != null && path.trim().length() > 0) {
            // There is no need to check for leading and trailing "/"
            final StringTokenizer tokenizer = new StringTokenizer(path, "/");
            while (tokenizer.hasMoreTokens()) {
                pathElements.add(tokenizer.nextToken().trim());
            }
        }
        return pathElements;
    }

    /**
     * Helper method that converts a map of String properties into a map of QName properties
     * @param props
     * @return a map of properties
     */
    public Map<QName, Serializable> mapToNodeProperties(Map<String, Object> properties) {
        Map<QName, Serializable> response = null;
        if (properties != null) {
            response = nodes.mapToNodeProperties(properties);
        }
        return response;
    }

    /**
     * Create an RM node
     *
     * @param parentNodeRef  the parent of the node
     * @param name  the name of the new node
     * @param type  the type of the node
     * @param properties properties to set on the new node
     * @param aspects aspects to set on the new node
     * @return the new node
     */
    public NodeRef createRMNode(NodeRef parentNodeRef, String name, String type, Map<String, Object> properties,
            List<String> aspects) {
        mandatory("parentNodeRef", parentNodeRef);
        checkNotBlank(RMNode.PARAM_NAME, name);
        checkNotBlank(RMNode.PARAM_NODE_TYPE, type);

        // Create the node
        NodeRef newNodeRef = null;
        try {
            QName typeQName = nodes.createQName(type);
            newNodeRef = fileFolderService.create(parentNodeRef, name, typeQName).getNodeRef();

            // Set the provided properties if any
            Map<QName, Serializable> qnameProperties = mapToNodeProperties(properties);
            if (qnameProperties != null) {
                nodeService.addProperties(newNodeRef, qnameProperties);
            }

            // If electronic record create empty content
            if (!typeQName.equals(RecordsManagementModel.TYPE_NON_ELECTRONIC_DOCUMENT)
                    && dictionaryService.isSubClass(typeQName, ContentModel.TYPE_CONTENT)) {
                writeContent(newNodeRef, name, new ByteArrayInputStream("".getBytes()), false);
            }

            // Add the provided aspects if any
            if (aspects != null) {
                nodes.addCustomAspects(newNodeRef, aspects, ApiNodesModelFactory.EXCLUDED_ASPECTS);
            }
        } catch (InvalidTypeException ex) {
            throw new InvalidArgumentException("The given type:'" + type + "' is invalid '");
        }

        return newNodeRef;
    }

    /**
     * Upload a record
     *
     * @param parentNodeRef the parent of the record
     * @param name the name of the record
     * @param type the type of the record (if null the record's type will be cm:content)
     * @param properties properties to set (can be null)
     * @param stream the stream to write
     * @return the new record
     */
    public NodeRef uploadRecord(NodeRef parentNodeRef, String name, String type, Map<String, Object> properties,
            InputStream stream) {
        checkNotBlank(RMNode.PARAM_NAME, name);
        mandatory("stream", stream);

        // Create the node
        QName typeQName = StringUtils.isBlank(type) ? ContentModel.TYPE_CONTENT : nodes.createQName(type);
        if (!dictionaryService.isSubClass(typeQName, ContentModel.TYPE_CONTENT)) {
            throw new InvalidArgumentException("Can only upload type of cm:content: " + typeQName);
        }
        NodeRef newNodeRef = fileFolderService.create(parentNodeRef, name, typeQName).getNodeRef();

        // Write content
        writeContent(newNodeRef, name, stream, true);

        // Set the provided properties if any
        Map<QName, Serializable> qnameProperties = mapToNodeProperties(properties);
        if (qnameProperties != null) {
            nodeService.addProperties(newNodeRef, qnameProperties);
        }

        return newNodeRef;
    }

    /**
     * Returns a List of filter properties specified by request parameters.
     * @param parameters The {@link Parameters} object to get the parameters passed into the request
     *        including:
     *        - filter, sort & paging params (where, orderBy, skipCount, maxItems)
     * @return The list of {@link FilterProp}. Can be null.
     */
    public List<FilterProp> getListChildrenFilterProps(Parameters parameters,
            Set<String> listFolderChildrenEqualsQueryProperties) {

        List<FilterProp> filterProps = null;
        Query q = parameters.getQuery();
        if (q != null) {
            MapBasedQueryWalker propertyWalker = new MapBasedQueryWalker(listFolderChildrenEqualsQueryProperties,
                    null);
            QueryHelper.walk(q, propertyWalker);

            Boolean isPrimary = propertyWalker.getProperty(RMNode.PARAM_ISPRIMARY, WhereClauseParser.EQUALS,
                    Boolean.class);

            if (isPrimary != null) {
                filterProps = new ArrayList<>(1);
                filterProps
                        .add(new FilterPropBoolean(GetChildrenCannedQuery.FILTER_QNAME_NODE_IS_PRIMARY, isPrimary));
            }
            Boolean isClosed = propertyWalker.getProperty(RMNode.PARAM_IS_CLOSED, WhereClauseParser.EQUALS,
                    Boolean.class);
            if (isClosed != null) {
                filterProps = new ArrayList<>(1);
                filterProps.add(new FilterPropBoolean(RecordsManagementModel.PROP_IS_CLOSED, isClosed));
            }
            //TODO see how we can filter for categories that have retention schedule
            //            Boolean hasRetentionSchedule = propertyWalker.getProperty(RMNode.PARAM_HAS_RETENTION_SCHEDULE, WhereClauseParser.EQUALS, Boolean.class);
            //            if (hasRetentionSchedule != null)
            //            {
            //                filterProps = new ArrayList<>(1);
            //            }
        }
        return filterProps;
    }

    /**
     * Utility method that updates a node's name and properties
     * @param nodeRef  the node to update
     * @param updateInfo  information to update the record with
     * @param parameters  request parameters
     */
    public void updateNode(NodeRef nodeRef, RMNode updateInfo, Parameters parameters) {
        Map<QName, Serializable> props = new HashMap<>(0);

        if (updateInfo.getProperties() != null) {
            props = mapToNodeProperties(updateInfo.getProperties());
        }

        String name = updateInfo.getName();
        if ((name != null) && (!name.isEmpty())) {
            // update node name if needed
            props.put(ContentModel.PROP_NAME, name);
        }

        try {
            // update node properties - note: null will unset the specified property
            nodeService.addProperties(nodeRef, props);
        } catch (DuplicateChildNodeNameException dcne) {
            throw new ConstraintViolatedException(dcne.getMessage());
        }

        // update aspects
        List<String> aspectNames = updateInfo.getAspectNames();
        nodes.updateCustomAspects(nodeRef, aspectNames, ApiNodesModelFactory.EXCLUDED_ASPECTS);
    }

    /**
     * Validates a record
     *
     * @param recordId  the id of the record to validate
     * @return
     */
    public NodeRef validateRecord(String recordId) throws InvalidArgumentException {
        NodeRef nodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, recordId);
        if (!recordService.isRecord(nodeRef)) {
            throw new IllegalArgumentException("The given id:'" + recordId
                    + "' is not valid for this endpoint. This endpoint only supports records.");
        }
        return nodeRef;
    }

    public BinaryResource getContent(NodeRef nodeRef, Parameters parameters, boolean recordActivity) {
        return nodes.getContent(nodeRef, parameters, recordActivity);
    }

    /**
     * Utility method that updates a transfer container's name and properties
     *
     * @param nodeRef  the node to update
     * @param transferContainerInfo  information to update the transfer container with
     * @param parameters  request parameters
     */
    public void updateTransferContainer(NodeRef nodeRef, TransferContainer transferContainerInfo,
            Parameters parameters) {
        Map<QName, Serializable> props = new HashMap<>(0);

        if (transferContainerInfo.getProperties() != null) {
            props = mapToNodeProperties(transferContainerInfo.getProperties());
        }

        String name = transferContainerInfo.getName();
        if ((name != null) && (!name.isEmpty())) {
            // update node name if needed
            props.put(ContentModel.PROP_NAME, name);
        }

        try {
            // update node properties - note: null will unset the specified property
            nodeService.addProperties(nodeRef, props);
        } catch (DuplicateChildNodeNameException dcne) {
            throw new ConstraintViolatedException(dcne.getMessage());
        }
    }

    /**
     * Helper method that generates allowable operation for the provided node
     * @param nodeRef the node to get the allowable operations for
     * @param type the type of the provided nodeRef
     * @return a sublist of [{@link Nodes.OP_DELETE}, {@link Nodes.OP_CREATE}, {@link Nodes.OP_UPDATE}] representing the allowable operations for the provided node
     */
    protected List<String> getAllowableOperations(NodeRef nodeRef, QName typeQName) {
        List<String> allowableOperations = new ArrayList<>();

        boolean isFilePlan = typeQName.equals(RecordsManagementModel.TYPE_FILE_PLAN);
        boolean isTransferContainer = typeQName.equals(RecordsManagementModel.TYPE_TRANSFER_CONTAINER);
        boolean isUnfiledContainer = typeQName.equals(RecordsManagementModel.TYPE_UNFILED_RECORD_CONTAINER);
        boolean isHoldsContainer = typeQName.equals(RecordsManagementModel.TYPE_HOLD_CONTAINER);
        boolean isSpecialContainer = isFilePlan || isTransferContainer || isUnfiledContainer || isHoldsContainer;

        // DELETE
        if (!isSpecialContainer && capabilityService.getCapability("Delete")
                .evaluate(nodeRef) == AccessDecisionVoter.ACCESS_GRANTED) {
            allowableOperations.add(Nodes.OP_DELETE);
        }

        // CREATE
        if (TYPES_CAN_CREATE.contains(typeQName) && capabilityService.getCapability("FillingPermissionOnly")
                .evaluate(nodeRef) == AccessDecisionVoter.ACCESS_GRANTED) {
            allowableOperations.add(Nodes.OP_CREATE);
        }

        // UPDATE
        if (capabilityService.getCapability("Update").evaluate(nodeRef) == AccessDecisionVoter.ACCESS_GRANTED) {
            allowableOperations.add(Nodes.OP_UPDATE);
        }

        return allowableOperations;
    }

    protected PathInfo lookupPathInfo(NodeRef nodeRefIn) {
        List<ElementInfo> pathElements = new ArrayList<>();
        Boolean isComplete = Boolean.TRUE;
        final Path nodePath = nodeService.getPath(nodeRefIn);
        ;
        final int pathIndex = 2;

        for (int i = nodePath.size() - pathIndex; i >= 0; i--) {
            Element element = nodePath.get(i);
            if (element instanceof Path.ChildAssocElement) {
                ChildAssociationRef elementRef = ((Path.ChildAssocElement) element).getRef();
                if (elementRef.getParentRef() != null) {
                    NodeRef childNodeRef = elementRef.getChildRef();
                    if (permissionService.hasPermission(childNodeRef,
                            PermissionService.READ) == AccessStatus.ALLOWED) {
                        Serializable nameProp = nodeService.getProperty(childNodeRef, ContentModel.PROP_NAME);
                        pathElements.add(0, new ElementInfo(childNodeRef.getId(), nameProp.toString()));
                    } else {
                        // Just return the pathInfo up to the location where the user has access
                        isComplete = Boolean.FALSE;
                        break;
                    }
                }
            }
        }

        String pathStr = null;
        if (pathElements.size() > 0) {
            StringBuilder sb = new StringBuilder(120);
            for (PathInfo.ElementInfo e : pathElements) {
                sb.append("/").append(e.getName());
            }
            pathStr = sb.toString();
        } else {
            // There is no path element, so set it to null in order to be
            // ignored by Jackson during serialisation
            isComplete = null;
        }
        return new PathInfo(pathStr, isComplete, pathElements);
    }

    /**
     * Helper method to obtain file plan type or null if the rm site does not exist.
     *
     * @return file plan type or null
     */
    public QName getFilePlanType() {
        NodeRef filePlanNodeRef = filePlanService.getFilePlanBySiteId(FilePlanService.DEFAULT_RM_SITE_ID);
        if (filePlanNodeRef != null) {
            return nodeService.getType(filePlanNodeRef);
        }
        return null;
    }

    /**
     * Posts activities for given fileInfo
     * 
     * @param fileInfo
     * @param parentNodeRef
     * @param activityType
     */
    public void postActivity(FileInfo fileInfo, NodeRef parentNodeRef, String activityType) {
        ActivityInfo activityInfo = null;
        RMSite rmSite = sites.getRMSite(RM_SITE_ID);
        if (rmSite != null && !rmSite.getId().equals("")) {
            if (fileInfo != null) {
                boolean isContent = dictionaryService.isSubClass(fileInfo.getType(), ContentModel.TYPE_CONTENT);

                if (isContent) {
                    activityInfo = new ActivityInfo(null, parentNodeRef, RM_SITE_ID, fileInfo);
                }
            }
        } else {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Non-site activity, so ignored " + fileInfo.getNodeRef());
            }
        }

        if (activityInfo == null)
            return; // Nothing to do.

        if (activityType != null) {
            activityPoster.postFileFolderActivity(activityType, null, TenantUtil.getCurrentDomain(),
                    activityInfo.getSiteId(), activityInfo.getParentNodeRef(), activityInfo.getNodeRef(),
                    activityInfo.getFileName(), Activities.APP_TOOL, Activities.RESTAPI_CLIENT,
                    activityInfo.getFileInfo());

        }
    }
}