/* * #%L * Alfresco Remote API * %% * Copyright (C) 2005 - 2016 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of * the paid license agreement will prevail. Otherwise, the software is * provided under the following open source license terms: * * Alfresco is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Alfresco is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Alfresco. If not, see . * #L% */ package org.alfresco.rest.api.impl; import java.io.InputStream; import java.io.Serializable; import java.math.BigInteger; import java.util.AbstractList; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; 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 java.util.StringTokenizer; import java.util.concurrent.ConcurrentHashMap; import org.alfresco.model.ApplicationModel; import org.alfresco.model.ContentModel; import org.alfresco.model.QuickShareModel; import org.alfresco.query.PagingRequest; import org.alfresco.query.PagingResults; import org.alfresco.repo.action.executer.ContentMetadataExtracter; import org.alfresco.repo.content.ContentLimitViolationException; import org.alfresco.repo.content.MimetypeMap; import org.alfresco.repo.model.Repository; import org.alfresco.repo.model.filefolder.FileFolderServiceImpl; import org.alfresco.repo.node.getchildren.GetChildrenCannedQuery; import org.alfresco.repo.policy.BehaviourFilter; 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.site.SiteModel; import org.alfresco.repo.site.SiteServiceInternal; import org.alfresco.repo.tenant.TenantUtil; import org.alfresco.repo.version.VersionModel; import org.alfresco.rest.antlr.WhereClauseParser; import org.alfresco.rest.api.Nodes; import org.alfresco.rest.api.QuickShareLinks; import org.alfresco.rest.api.model.ContentInfo; import org.alfresco.rest.api.model.Document; import org.alfresco.rest.api.model.Folder; import org.alfresco.rest.api.model.Node; import org.alfresco.rest.api.model.PathInfo; import org.alfresco.rest.api.model.PathInfo.ElementInfo; import org.alfresco.rest.api.model.QuickShareLink; import org.alfresco.rest.api.model.UserInfo; import org.alfresco.rest.framework.core.exceptions.ApiException; 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.PermissionDeniedException; import org.alfresco.rest.framework.core.exceptions.RequestEntityTooLargeException; import org.alfresco.rest.framework.resource.content.BasicContentInfo; import org.alfresco.rest.framework.resource.content.BinaryResource; import org.alfresco.rest.framework.resource.content.ContentInfoImpl; import org.alfresco.rest.framework.resource.content.NodeBinaryResource; import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; import org.alfresco.rest.framework.resource.parameters.Paging; import org.alfresco.rest.framework.resource.parameters.Parameters; import org.alfresco.rest.framework.resource.parameters.SortColumn; 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.service.ServiceRegistry; import org.alfresco.service.cmr.action.Action; import org.alfresco.service.cmr.action.ActionDefinition; import org.alfresco.service.cmr.action.ActionService; import org.alfresco.service.cmr.dictionary.AspectDefinition; import org.alfresco.service.cmr.dictionary.DataTypeDefinition; import org.alfresco.service.cmr.dictionary.DictionaryService; 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.ChildAssociationRef; import org.alfresco.service.cmr.repository.ContentData; import org.alfresco.service.cmr.repository.ContentService; import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.cmr.repository.CyclicChildRelationshipException; import org.alfresco.service.cmr.repository.DuplicateChildNodeNameException; import org.alfresco.service.cmr.repository.InvalidNodeRefException; 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.AuthorityService; import org.alfresco.service.cmr.security.OwnableService; import org.alfresco.service.cmr.security.PermissionService; import org.alfresco.service.cmr.security.PersonService; import org.alfresco.service.cmr.usage.ContentQuotaException; import org.alfresco.service.cmr.version.VersionService; import org.alfresco.service.cmr.version.VersionType; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; import org.alfresco.util.Pair; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.dao.ConcurrencyFailureException; import org.springframework.extensions.surf.util.Content; import org.springframework.extensions.webscripts.servlet.FormData; import org.springframework.http.InvalidMediaTypeException; import org.springframework.http.MediaType; /** * Centralises access to file/folder/node services and maps between representations. * * Note: * This class was originally used for returning some basic node info when listing Favourites. * * It has now been re-purposed and extended to implement the new Nodes (RESTful) API for * managing files & folders, as well as custom node types. * * @author steveglover * @author janv * @author Jamal Kaabi-Mofrad * * @since publicapi1.0 */ public class NodesImpl implements Nodes { private static final Log logger = LogFactory.getLog(NodesImpl.class); private enum Type { // Note: ordered DOCUMENT, FOLDER } private static final String DEFAULT_MIMETYPE = MimetypeMap.MIMETYPE_BINARY; private NodeService nodeService; private DictionaryService dictionaryService; private FileFolderService fileFolderService; private NamespaceService namespaceService; private PermissionService permissionService; private MimetypeService mimetypeService; private ContentService contentService; private ActionService actionService; private VersionService versionService; private PersonService personService; private OwnableService ownableService; private AuthorityService authorityService; private BehaviourFilter behaviourFilter; // note: circular - Nodes/QuickShareLinks currently use each other (albeit for different methods) private QuickShareLinks quickShareLinks; private Repository repositoryHelper; private ServiceRegistry sr; private Set defaultIgnoreTypesAndAspects; // ignore types/aspects private Set ignoreQNames; private ConcurrentHashMap ddCache = new ConcurrentHashMap(); private Set nonAttachContentTypes = Collections.emptySet(); // pre-configured whitelist, eg. images & pdf public void setNonAttachContentTypes(Set nonAttachWhiteList) { this.nonAttachContentTypes = nonAttachWhiteList; } public void init() { this.namespaceService = sr.getNamespaceService(); this.fileFolderService = sr.getFileFolderService(); this.nodeService = sr.getNodeService(); this.permissionService = sr.getPermissionService(); this.dictionaryService = sr.getDictionaryService(); this.mimetypeService = sr.getMimetypeService(); this.contentService = sr.getContentService(); this.actionService = sr.getActionService(); this.versionService = sr.getVersionService(); this.personService = sr.getPersonService(); this.ownableService = sr.getOwnableService(); this.authorityService = sr.getAuthorityService(); if (defaultIgnoreTypesAndAspects != null) { ignoreQNames = new HashSet<>(defaultIgnoreTypesAndAspects.size()); for (String type : defaultIgnoreTypesAndAspects) { ignoreQNames.add(createQName(type)); } } } public void setServiceRegistry(ServiceRegistry sr) { this.sr = sr; } public void setBehaviourFilter(BehaviourFilter behaviourFilter) { this.behaviourFilter = behaviourFilter; } public void setRepositoryHelper(Repository repositoryHelper) { this.repositoryHelper = repositoryHelper; } public void setQuickShareLinks(QuickShareLinks quickShareLinks) { this.quickShareLinks = quickShareLinks; } public void setIgnoreTypes(Set ignoreTypesAndAspects) { this.defaultIgnoreTypesAndAspects = ignoreTypesAndAspects; } // excluded namespaces (aspects and properties) private static final List EXCLUDED_NS = Arrays.asList(NamespaceService.SYSTEM_MODEL_1_0_URI); // excluded aspects private static final List EXCLUDED_ASPECTS = Arrays.asList(); // excluded properties private static final List EXCLUDED_PROPS = Arrays.asList( // top-level minimal info ContentModel.PROP_NAME, ContentModel.PROP_MODIFIER, ContentModel.PROP_MODIFIED, ContentModel.PROP_CREATOR, ContentModel.PROP_CREATED, ContentModel.PROP_CONTENT, // other - TBC ContentModel.PROP_INITIAL_VERSION, ContentModel.PROP_AUTO_VERSION_PROPS, ContentModel.PROP_AUTO_VERSION); private static final List PROPS_USERLOOKUP = Arrays.asList( ContentModel.PROP_CREATOR, ContentModel.PROP_MODIFIER, ContentModel.PROP_OWNER, ContentModel.PROP_LOCK_OWNER, ContentModel.PROP_WORKING_COPY_OWNER); private final static Map MAP_PARAM_QNAME; static { Map aMap = new HashMap<>(9); aMap.put(PARAM_ISFOLDER, GetChildrenCannedQuery.SORT_QNAME_NODE_IS_FOLDER); aMap.put(PARAM_NAME, ContentModel.PROP_NAME); aMap.put(PARAM_CREATEDAT, ContentModel.PROP_CREATED); aMap.put(PARAM_MODIFIEDAT, ContentModel.PROP_MODIFIED); aMap.put(PARAM_CREATEBYUSER, ContentModel.PROP_CREATOR); aMap.put(PARAM_MODIFIEDBYUSER, ContentModel.PROP_MODIFIER); aMap.put(PARAM_MIMETYPE, GetChildrenCannedQuery.SORT_QNAME_CONTENT_MIMETYPE); aMap.put(PARAM_SIZEINBYTES, GetChildrenCannedQuery.SORT_QNAME_CONTENT_SIZE); aMap.put(PARAM_NODETYPE, GetChildrenCannedQuery.SORT_QNAME_NODE_TYPE); MAP_PARAM_QNAME = Collections.unmodifiableMap(aMap); } // list children filtering (via where clause) private final static Set LIST_FOLDER_CHILDREN_EQUALS_QUERY_PROPERTIES = new HashSet<>(Arrays.asList(new String[] {PARAM_ISFOLDER, PARAM_ISFILE, PARAM_NODETYPE})); /* * Validates that node exists. * * Note: assumes workspace://SpacesStore */ @Override public NodeRef validateNode(String nodeId) { return validateNode(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, nodeId); } @Override public NodeRef validateNode(StoreRef storeRef, String nodeId) { String versionLabel = null; int idx = nodeId.indexOf(";"); if (idx != -1) { versionLabel = nodeId.substring(idx + 1); nodeId = nodeId.substring(0, idx); if (versionLabel.equals("pwc")) { // TODO correct exception? throw new EntityNotFoundException(nodeId); } } NodeRef nodeRef = new NodeRef(storeRef, nodeId); return validateNode(nodeRef); } @Override public NodeRef validateNode(NodeRef nodeRef) { if (!nodeService.exists(nodeRef)) { throw new EntityNotFoundException(nodeRef.getId()); } return nodeRef; } /* * Check that nodes exists and matches given expected/excluded type(s). */ @Override public boolean nodeMatches(NodeRef nodeRef, Set expectedTypes, Set excludedTypes) { return nodeMatches(nodeRef, expectedTypes, excludedTypes, true); } @Override public boolean isSubClass(NodeRef nodeRef, QName ofClassQName, boolean validateNodeRef) { if (validateNodeRef) { nodeRef = validateNode(nodeRef); } return isSubClass(getNodeType(nodeRef), ofClassQName); } private boolean nodeMatches(NodeRef nodeRef, Set expectedTypes, Set excludedTypes, boolean existsCheck) { if (existsCheck && (! nodeService.exists(nodeRef))) { throw new EntityNotFoundException(nodeRef.getId()); } return typeMatches(getNodeType(nodeRef), expectedTypes, excludedTypes); } private QName getNodeType(NodeRef nodeRef) { return nodeService.getType(nodeRef); } private boolean isSubClass(QName className, QName ofClassQName) { return dictionaryService.isSubClass(className, ofClassQName); } protected boolean typeMatches(QName type, Set expectedTypes, Set excludedTypes) { if (((expectedTypes != null) && (expectedTypes.size() == 1)) && ((excludedTypes == null) || (excludedTypes.size() == 0))) { // use isSubClass if checking against single expected type (and no excluded types) return isSubClass(type, expectedTypes.iterator().next()); } Set allExpectedTypes = new HashSet<>(); if (expectedTypes != null) { for (QName expectedType : expectedTypes) { allExpectedTypes.addAll(dictionaryService.getSubTypes(expectedType, true)); } } Set allExcludedTypes = new HashSet<>(); if (excludedTypes != null) { for (QName excludedType : excludedTypes) { allExcludedTypes.addAll(dictionaryService.getSubTypes(excludedType, true)); } } boolean inExpected = allExpectedTypes.contains(type); boolean excluded = allExcludedTypes.contains(type); return (inExpected && !excluded); } /** * @deprecated review usage (backward compat') */ @Override public Node getNode(String nodeId) { NodeRef nodeRef = validateNode(nodeId); return new Node(nodeRef, null, nodeService.getProperties(nodeRef), null, sr); } /** * @deprecated review usage (backward compat') */ public Node getNode(NodeRef nodeRef) { return new Node(nodeRef, null, nodeService.getProperties(nodeRef), null, sr); } private Type getType(NodeRef nodeRef) { return getType(getNodeType(nodeRef), nodeRef); } private Type getType(QName typeQName, NodeRef nodeRef) { // quick check for common types if (typeQName.equals(ContentModel.TYPE_FOLDER) || typeQName.equals(ApplicationModel.TYPE_FOLDERLINK)) { return Type.FOLDER; } else if (typeQName.equals(ContentModel.TYPE_CONTENT) || typeQName.equals(ApplicationModel.TYPE_FILELINK)) { return Type.DOCUMENT; } // further checks if (isSubClass(typeQName, ContentModel.TYPE_LINK)) { if (isSubClass(typeQName, ApplicationModel.TYPE_FOLDERLINK)) { return Type.FOLDER; } else if (isSubClass(typeQName, ApplicationModel.TYPE_FILELINK)) { return Type.DOCUMENT; } NodeRef linkNodeRef = (NodeRef)nodeService.getProperty(nodeRef, ContentModel.PROP_LINK_DESTINATION); if (linkNodeRef != null) { try { typeQName = getNodeType(linkNodeRef); // drop-through to check type of destination // note: edge-case - if link points to another link then we will return null } catch (InvalidNodeRefException inre) { // ignore } } } if (isSubClass(typeQName, ContentModel.TYPE_FOLDER)) { if (! isSubClass(typeQName, ContentModel.TYPE_SYSTEM_FOLDER)) { return Type.FOLDER; } return null; // unknown } else if (isSubClass(typeQName, ContentModel.TYPE_CONTENT)) { return Type.DOCUMENT; } return null; // unknown } /** * @deprecated note: currently required for backwards compat' (Favourites API) */ @Override public Document getDocument(NodeRef nodeRef) { Type type = getType(nodeRef); if ((type != null) && type.equals(Type.DOCUMENT)) { Map properties = nodeService.getProperties(nodeRef); Document doc = new Document(nodeRef, getParentNodeRef(nodeRef), properties, null, sr); doc.setVersionLabel((String) properties.get(ContentModel.PROP_VERSION_LABEL)); ContentData cd = (ContentData) properties.get(ContentModel.PROP_CONTENT); if (cd != null) { doc.setSizeInBytes(BigInteger.valueOf(cd.getSize())); doc.setMimeType((cd.getMimetype())); } setCommonProps(doc, nodeRef, properties); return doc; } else { throw new InvalidArgumentException("Node is not a file: "+nodeRef.getId()); } } private void setCommonProps(Node node, NodeRef nodeRef, Map properties) { node.setGuid(nodeRef); node.setTitle((String)properties.get(ContentModel.PROP_TITLE)); node.setDescription((String)properties.get(ContentModel.PROP_TITLE)); node.setModifiedBy((String)properties.get(ContentModel.PROP_MODIFIER)); node.setCreatedBy((String)properties.get(ContentModel.PROP_CREATOR)); } /** * @deprecated note: currently required for backwards compat' (Favourites API) */ @Override public Folder getFolder(NodeRef nodeRef) { Type type = getType(nodeRef); if ((type != null) && type.equals(Type.FOLDER)) { Map properties = nodeService.getProperties(nodeRef); Folder folder = new Folder(nodeRef, getParentNodeRef(nodeRef), properties, null, sr); setCommonProps(folder, nodeRef, properties); return folder; } else { throw new InvalidArgumentException("Node is not a folder: "+nodeRef.getId()); } } private NodeRef getParentNodeRef(NodeRef nodeRef) { if (repositoryHelper.getCompanyHome().equals(nodeRef)) { return null; // note: does not make sense to return parent above C/H } return nodeService.getPrimaryParent(nodeRef).getParentRef(); } protected NodeRef validateOrLookupNode(String nodeId, String path) { NodeRef parentNodeRef; if ((nodeId == null) || (nodeId.isEmpty())) { throw new InvalidArgumentException("Missing nodeId"); } if (nodeId.equals(PATH_ROOT)) { parentNodeRef = repositoryHelper.getCompanyHome(); } else if (nodeId.equals(PATH_SHARED)) { parentNodeRef = repositoryHelper.getSharedHome(); } else if (nodeId.equals(PATH_MY)) { NodeRef person = repositoryHelper.getPerson(); if (person == null) { throw new InvalidArgumentException("Unexpected - cannot use: " + PATH_MY); } parentNodeRef = repositoryHelper.getUserHome(person); if (parentNodeRef == null) { throw new EntityNotFoundException(nodeId); } } else { parentNodeRef = validateNode(nodeId); } if (path != null) { // check that parent is a folder before resolving relative path if (! nodeMatches(parentNodeRef, Collections.singleton(ContentModel.TYPE_FOLDER), null, false)) { throw new InvalidArgumentException("NodeId of folder is expected: "+parentNodeRef.getId()); } // resolve path relative to current nodeId parentNodeRef = resolveNodeByPath(parentNodeRef, path, true); } return parentNodeRef; } protected NodeRef resolveNodeByPath(final NodeRef parentNodeRef, String path, boolean checkForCompanyHome) { final List pathElements = getPathElements(path); if (!pathElements.isEmpty() && checkForCompanyHome) { /* if (nodeService.getRootNode(parentNodeRef.getStoreRef()).equals(parentNodeRef)) { // special case NodeRef chNodeRef = repositoryHelper.getCompanyHome(); String chName = (String) nodeService.getProperty(chNodeRef, ContentModel.PROP_NAME); if (chName.equals(pathElements.get(0))) { pathElements = pathElements.subList(1, pathElements.size()); parentNodeRef = chNodeRef; } } */ } FileInfo fileInfo = null; try { if (!pathElements.isEmpty()) { fileInfo = fileFolderService.resolveNamePath(parentNodeRef, pathElements); } else { fileInfo = fileFolderService.getFileInfo(parentNodeRef); if (fileInfo == null) { throw new FileNotFoundException(parentNodeRef); } } } catch (FileNotFoundException fnfe) { // convert checked exception throw new EntityNotFoundException(parentNodeRef.getId()); } return fileInfo.getNodeRef(); } private List getPathElements(String path) { final List 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; } private NodeRef makeFolders(NodeRef parentNodeRef, List pathElements) { NodeRef currentParentRef = parentNodeRef; // just loop and create if necessary for (final String element : pathElements) { final NodeRef contextNodeRef = currentParentRef; // does it exist? // Navigation should not check permissions NodeRef nodeRef = AuthenticationUtil.runAs(new RunAsWork() { @Override public NodeRef doWork() throws Exception { return nodeService.getChildByName(contextNodeRef, ContentModel.ASSOC_CONTAINS, element); } }, AuthenticationUtil.getSystemUserName()); if (nodeRef == null) { try { // Checks for create permissions as the fileFolderService is a public service. FileInfo createdFileInfo = fileFolderService.create(currentParentRef, element, ContentModel.TYPE_FOLDER); currentParentRef = createdFileInfo.getNodeRef(); } catch (AccessDeniedException ade) { throw new PermissionDeniedException(ade.getMessage()); } catch (FileExistsException fex) { // Assume concurrency failure, so retry throw new ConcurrencyFailureException(fex.getMessage()); } } else if (!isSubClass(nodeRef, ContentModel.TYPE_FOLDER, false)) { String parentName = (String) nodeService.getProperty(contextNodeRef, ContentModel.PROP_NAME); throw new ConstraintViolatedException("Name [" + element + "] already exists in the target parent: " + parentName); } else { // it exists currentParentRef = nodeRef; } } return currentParentRef; } @Override public Node getFolderOrDocument(String nodeId, Parameters parameters) { String path = parameters.getParameter(PARAM_RELATIVE_PATH); NodeRef nodeRef = validateOrLookupNode(nodeId, path); return getFolderOrDocumentFullInfo(nodeRef, null, null, parameters); } private Node getFolderOrDocumentFullInfo(NodeRef nodeRef, NodeRef parentNodeRef, QName nodeTypeQName, Parameters parameters) { return getFolderOrDocumentFullInfo(nodeRef, parentNodeRef, nodeTypeQName, parameters, null); } private Node getFolderOrDocumentFullInfo(NodeRef nodeRef, NodeRef parentNodeRef, QName nodeTypeQName, Parameters parameters, Map mapUserInfo) { List includeParam = new ArrayList<>(); if (parameters != null) { includeParam.addAll(parameters.getInclude()); } // Add basic info for single get (above & beyond minimal that is used for listing collections) includeParam.add(PARAM_INCLUDE_ASPECTNAMES); includeParam.add(PARAM_INCLUDE_PROPERTIES); return getFolderOrDocument(nodeRef, parentNodeRef, nodeTypeQName, includeParam, mapUserInfo); } private Node getFolderOrDocument(final NodeRef nodeRef, NodeRef parentNodeRef, QName nodeTypeQName, List includeParam, Map mapUserInfo) { if (mapUserInfo == null) { mapUserInfo = new HashMap<>(2); } PathInfo pathInfo = null; if (includeParam.contains(PARAM_INCLUDE_PATH)) { pathInfo = lookupPathInfo(nodeRef); } if (nodeTypeQName == null) { nodeTypeQName = getNodeType(nodeRef); } if (parentNodeRef == null) { parentNodeRef = getParentNodeRef(nodeRef); } Node node; Map properties = nodeService.getProperties(nodeRef); Type type = getType(nodeTypeQName, nodeRef); if (type == null) { // not direct folder (or file) ... // might be sub-type of cm:cmobject (or a cm:link pointing to cm:cmobject or possibly even another cm:link) node = new Node(nodeRef, parentNodeRef, properties, mapUserInfo, sr); node.setIsFolder(false); node.setIsFile(false); } else if (type.equals(Type.DOCUMENT)) { node = new Document(nodeRef, parentNodeRef, properties, mapUserInfo, sr); } else if (type.equals(Type.FOLDER)) { node = new Folder(nodeRef, parentNodeRef, properties, mapUserInfo, sr); } else { throw new RuntimeException("Unexpected - should not reach here: "+type); } if (includeParam.size() > 0) { node.setProperties(mapFromNodeProperties(properties, includeParam, mapUserInfo)); } if (includeParam.contains(PARAM_INCLUDE_ASPECTNAMES)) { node.setAspectNames(mapFromNodeAspects(nodeService.getAspects(nodeRef))); } if (includeParam.contains(PARAM_INCLUDE_ISLINK)) { boolean isLink = isSubClass(nodeTypeQName, ContentModel.TYPE_LINK); node.setIsLink(isLink); } if (includeParam.contains(PARAM_INCLUDE_ALLOWABLEOPERATIONS)) { // note: refactor when requirements change Map mapPermsToOps = new HashMap<>(3); mapPermsToOps.put(PermissionService.DELETE, OP_DELETE); mapPermsToOps.put(PermissionService.ADD_CHILDREN, OP_CREATE); mapPermsToOps.put(PermissionService.WRITE, OP_UPDATE); List allowableOperations = new ArrayList<>(3); for (Entry kv : mapPermsToOps.entrySet()) { String perm = kv.getKey(); String op = kv.getValue(); if (perm.equals(PermissionService.ADD_CHILDREN) && type.equals(Type.DOCUMENT)) { // special case: do not return "create" (as an allowable op) for file/content types continue; } else if (perm.equals(PermissionService.DELETE) && (isSpecialNodeDoNotDelete(nodeRef, nodeTypeQName))) { // special case: do not return "delete" (as an allowable op) for specific system nodes continue; } else if (permissionService.hasPermission(nodeRef, perm) == AccessStatus.ALLOWED) { allowableOperations.add(op); } } node.setAllowableOperations((allowableOperations.size() > 0 )? allowableOperations : null); } node.setNodeType(nodeTypeQName.toPrefixString(namespaceService)); node.setPath(pathInfo); return node; } protected PathInfo lookupPathInfo(NodeRef nodeRefIn) { final Path nodePath = nodeService.getPath(nodeRefIn); List pathElements = new ArrayList<>(); Boolean isComplete = Boolean.TRUE; // 2 => as we don't want to include the given node in the path as well. for (int i = nodePath.size() - 2; 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); } protected Set mapToNodeAspects(List aspectNames) { Set nodeAspects = new HashSet<>(aspectNames.size()); for (String aspectName : aspectNames) { QName aspectQName = createQName(aspectName); AspectDefinition ad = dictionaryService.getAspect(aspectQName); if (ad != null) { nodeAspects.add(aspectQName); } else { throw new InvalidArgumentException("Unknown aspect: " + aspectName); } } return nodeAspects; } protected Map mapToNodeProperties(Map props) { Map nodeProps = new HashMap<>(props.size()); for (Entry entry : props.entrySet()) { String propName = entry.getKey(); QName propQName = createQName(propName); PropertyDefinition pd = dictionaryService.getProperty(propQName); if (pd != null) { Serializable value; if (pd.getDataType().getName().equals(DataTypeDefinition.NODE_REF)) { String nodeRefString = (String) entry.getValue(); if (! NodeRef.isNodeRef(nodeRefString)) { value = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, nodeRefString); } else { value = new NodeRef(nodeRefString); } } else { value = (Serializable)entry.getValue(); } nodeProps.put(propQName, value); } else { throw new InvalidArgumentException("Unknown property: " + propName); } } return nodeProps; } protected Map mapFromNodeProperties(Map nodeProps, List selectParam, Map mapUserInfo) { List selectedProperties; if ((selectParam.size() == 0) || selectParam.contains(PARAM_INCLUDE_PROPERTIES)) { // return all properties selectedProperties = new ArrayList<>(nodeProps.size()); for (QName propQName : nodeProps.keySet()) { if ((! EXCLUDED_NS.contains(propQName.getNamespaceURI())) && (! EXCLUDED_PROPS.contains(propQName))) { selectedProperties.add(propQName); } } } else { // return selected properties selectedProperties = createQNames(selectParam); } Map props = null; if (!selectedProperties.isEmpty()) { props = new HashMap<>(selectedProperties.size()); for (QName qName : selectedProperties) { Serializable value = nodeProps.get(qName); if (value != null) { if (PROPS_USERLOOKUP.contains(qName)) { value = Node.lookupUserInfo((String)value, mapUserInfo, sr.getPersonService()); } props.put(qName.toPrefixString(namespaceService), value); } } if (props.isEmpty()) { props = null; // set to null so it doesn't show up as an empty object in the JSON response. } } return props; } protected List mapFromNodeAspects(Set nodeAspects) { List aspectNames = new ArrayList<>(nodeAspects.size()); for (QName aspectQName : nodeAspects) { if ((! EXCLUDED_NS.contains(aspectQName.getNamespaceURI())) && (! EXCLUDED_ASPECTS.contains(aspectQName))) { aspectNames.add(aspectQName.toPrefixString(namespaceService)); } } if (aspectNames.size() == 0) { aspectNames = null; // no aspects to return } return aspectNames; } @Override public CollectionWithPagingInfo listChildren(String parentFolderNodeId, Parameters parameters) { String path = parameters.getParameter(PARAM_RELATIVE_PATH); final NodeRef parentNodeRef = validateOrLookupNode(parentFolderNodeId, path); // check that resolved node is a folder if (!nodeMatches(parentNodeRef, Collections.singleton(ContentModel.TYPE_FOLDER), null, false)) { throw new InvalidArgumentException("NodeId of folder is expected: " + parentNodeRef.getId()); } final List includeParam = parameters.getInclude(); Boolean includeFolders = null; Boolean includeFiles = null; QName filterNodeTypeQName = null; // note: for files/folders, include subtypes by default (unless filtering by a specific nodeType - see below) boolean filterIncludeSubTypes = true; Query q = parameters.getQuery(); if (q != null) { // filtering via "where" clause MapBasedQueryWalker propertyWalker = new MapBasedQueryWalker(LIST_FOLDER_CHILDREN_EQUALS_QUERY_PROPERTIES, null); QueryHelper.walk(q, propertyWalker); Boolean isFolder = propertyWalker.getProperty(PARAM_ISFOLDER, WhereClauseParser.EQUALS, Boolean.class); Boolean isFile = propertyWalker.getProperty(PARAM_ISFILE, WhereClauseParser.EQUALS, Boolean.class); if (isFolder != null) { includeFolders = isFolder; } if (isFile != null) { includeFiles = isFile; } String nodeTypeStr = propertyWalker.getProperty(PARAM_NODETYPE, WhereClauseParser.EQUALS, String.class); if ((nodeTypeStr != null) && (! nodeTypeStr.isEmpty())) { Pair pair = parseNodeTypeFilter(nodeTypeStr); filterNodeTypeQName = pair.getFirst(); filterIncludeSubTypes = pair.getSecond(); } } List sortCols = parameters.getSorting(); List> sortProps = null; if ((sortCols != null) && (sortCols.size() > 0)) { // TODO should we allow isFile in sort (and map to reverse of isFolder) ? sortProps = new ArrayList<>(sortCols.size()); for (SortColumn sortCol : sortCols) { QName propQname = MAP_PARAM_QNAME.get(sortCol.column); if (propQname == null) { propQname = createQName(sortCol.column); } if (propQname != null) { sortProps.add(new Pair<>(propQname, sortCol.asc)); } } } else { // default sort order sortProps = new ArrayList<>(Arrays.asList( new Pair<>(GetChildrenCannedQuery.SORT_QNAME_NODE_IS_FOLDER, Boolean.FALSE), new Pair<>(ContentModel.PROP_NAME, true))); } Paging paging = parameters.getPaging(); PagingRequest pagingRequest = Util.getPagingRequest(paging); final PagingResults pagingResults; if ((filterNodeTypeQName != null) || (Boolean.FALSE.equals(includeFiles) && Boolean.FALSE.equals(includeFolders))) { if (filterNodeTypeQName == null) { filterNodeTypeQName = ContentModel.TYPE_CMOBJECT; } Pair, Set> pair = buildSearchTypesAndIgnoreAspects(filterNodeTypeQName, filterIncludeSubTypes, ignoreQNames, includeFiles, includeFolders); Set searchTypeQNames = pair.getFirst(); Set ignoreAspectQNames = pair.getSecond(); pagingResults = fileFolderService.list(parentNodeRef, searchTypeQNames, ignoreAspectQNames, sortProps, pagingRequest); } else { // files and/or folders only if ((includeFiles == null) && (includeFolders == null)) { // no filtering includeFiles = true; includeFolders = true; } else { // some filtering includeFiles = (includeFiles != null ? includeFiles : false); includeFolders = (includeFolders != null ? includeFolders : false); } pagingResults = fileFolderService.list(parentNodeRef, includeFiles, includeFolders, ignoreQNames, sortProps, pagingRequest); } final Map mapUserInfo = new HashMap<>(10); final List page = pagingResults.getPage(); List nodes = new AbstractList() { @Override public Node get(int index) { FileInfo fInfo = page.get(index); // minimal info by default (unless "select"ed otherwise) return getFolderOrDocument(fInfo.getNodeRef(), parentNodeRef, fInfo.getType(), includeParam, mapUserInfo); } @Override public int size() { return page.size(); } }; Node sourceEntity = null; if (parameters.includeSource()) { sourceEntity = getFolderOrDocumentFullInfo(parentNodeRef, null, null, null, mapUserInfo); } return CollectionWithPagingInfo.asPaged(paging, nodes, pagingResults.hasMoreItems(), pagingResults.getTotalResultCount().getFirst(), sourceEntity); } private Pair parseNodeTypeFilter(String nodeTypeStr) { boolean filterIncludeSubTypes = false; // default nodeType filtering is without subTypes (unless nodeType value is suffixed with ' INCLUDESUBTYPES') int idx = nodeTypeStr.lastIndexOf(' '); if (idx > 0) { String suffix = nodeTypeStr.substring(idx); if (suffix.equalsIgnoreCase(" "+PARAM_INCLUDE_SUBTYPES)) { filterIncludeSubTypes = true; nodeTypeStr = nodeTypeStr.substring(0, idx); } } QName filterNodeTypeQName = createQName(nodeTypeStr); if (dictionaryService.getType(filterNodeTypeQName) == null) { throw new InvalidArgumentException("Unknown filter nodeType: "+nodeTypeStr); } return new Pair<>(filterNodeTypeQName, filterIncludeSubTypes); } protected Pair, Set> buildSearchTypesAndIgnoreAspects(QName nodeTypeQName, boolean includeSubTypes, Set ignoreQNameTypes, Boolean includeFiles, Boolean includeFolders) { Set searchTypeQNames = new HashSet(100); Set ignoreAspectQNames = null; // Build a list of (sub-)types if (includeSubTypes) { Collection qnames = dictionaryService.getSubTypes(nodeTypeQName, true); searchTypeQNames.addAll(qnames); } searchTypeQNames.add(nodeTypeQName); // Remove 'system' folders if (includeSubTypes) { Collection qnames = dictionaryService.getSubTypes(ContentModel.TYPE_SYSTEM_FOLDER, true); searchTypeQNames.removeAll(qnames); } searchTypeQNames.remove(ContentModel.TYPE_SYSTEM_FOLDER); if (includeFiles != null) { if (includeFiles) { if (! dictionaryService.isSubClass(ContentModel.TYPE_CONTENT, nodeTypeQName)) { throw new InvalidArgumentException("Cannot filter for isFile since not sub-type of: "+nodeTypeQName); } if (includeSubTypes) { Collection qnames = dictionaryService.getSubTypes(ContentModel.TYPE_CONTENT, true); searchTypeQNames.addAll(qnames); } searchTypeQNames.add(ContentModel.TYPE_CONTENT); } else { Collection qnames = dictionaryService.getSubTypes(ContentModel.TYPE_CONTENT, true); searchTypeQNames.removeAll(qnames); searchTypeQNames.remove(ContentModel.TYPE_CONTENT); } } if (includeFolders != null) { if (includeFolders) { if (! dictionaryService.isSubClass(ContentModel.TYPE_FOLDER, nodeTypeQName)) { throw new InvalidArgumentException("Cannot filter for isFolder since not sub-type of: "+nodeTypeQName); } if (includeSubTypes) { Collection qnames = dictionaryService.getSubTypes(ContentModel.TYPE_FOLDER, true); searchTypeQNames.addAll(qnames); } searchTypeQNames.add(ContentModel.TYPE_FOLDER); } else { Collection qnames = dictionaryService.getSubTypes(ContentModel.TYPE_FOLDER, true); searchTypeQNames.removeAll(qnames); searchTypeQNames.remove(ContentModel.TYPE_FOLDER); } } if (ignoreQNameTypes != null) { Set ignoreQNamesNotSearchTypes = new HashSet(ignoreQNameTypes); ignoreQNamesNotSearchTypes.removeAll(searchTypeQNames); ignoreQNamesNotSearchTypes.remove(ContentModel.TYPE_SYSTEM_FOLDER); if (ignoreQNamesNotSearchTypes.size() > 0) { ignoreAspectQNames = getAspectsToIgnore(ignoreQNamesNotSearchTypes); } searchTypeQNames.removeAll(ignoreQNameTypes); } return new Pair<>(searchTypeQNames, ignoreAspectQNames); } private Set getAspectsToIgnore(Set ignoreQNames) { Set ignoreQNameAspects = new HashSet(ignoreQNames.size()); for (QName qname : ignoreQNames) { if (dictionaryService.getAspect(qname) != null) { ignoreQNameAspects.add(qname); } } return ignoreQNameAspects; } @Override public void deleteNode(String nodeId, Parameters parameters) { NodeRef nodeRef = validateOrLookupNode(nodeId, null); if (isSpecialNodeDoNotDelete(nodeRef, getNodeType(nodeRef))) { throw new PermissionDeniedException("Cannot delete: " + nodeId); } // default false (if not provided) boolean permanentDelete = Boolean.valueOf(parameters.getParameter(PARAM_PERMANENT)); if (permanentDelete == true) { boolean isAdmin = authorityService.hasAdminAuthority(); if (! isAdmin) { String owner = ownableService.getOwner(nodeRef); if (! AuthenticationUtil.getRunAsUser().equals(owner)) { // non-owner/non-admin cannot permanently delete (even if they have delete permission) throw new PermissionDeniedException("Non-owner/non-admin cannot permanently delete: " + nodeId); } } // Set as temporary to delete node instead of archiving. nodeService.addAspect(nodeRef, ContentModel.ASPECT_TEMPORARY, null); } fileFolderService.delete(nodeRef); } // TODO should we able to specify content properties (eg. mimeType ... or use extension for now, or encoding) @Override public Node createNode(String parentFolderNodeId, Node nodeInfo, Parameters parameters) { if (nodeInfo.getNodeRef() != null) { throw new InvalidArgumentException("Unexpected id when trying to create a new node: "+nodeInfo.getNodeRef().getId()); } // check that requested parent node exists and it's type is a (sub-)type of folder NodeRef parentNodeRef = validateOrLookupNode(parentFolderNodeId, null); if (! nodeMatches(parentNodeRef, Collections.singleton(ContentModel.TYPE_FOLDER), null, false)) { throw new InvalidArgumentException("NodeId of folder is expected: "+parentNodeRef.getId()); } // node name - mandatory String nodeName = nodeInfo.getName(); if ((nodeName == null) || nodeName.isEmpty()) { throw new InvalidArgumentException("Node name is expected: "+parentNodeRef.getId()); } // node type - check that requested type is a (sub-) type of cm:object String nodeType = nodeInfo.getNodeType(); if ((nodeType == null) || nodeType.isEmpty()) { throw new InvalidArgumentException("Node type is expected: "+parentNodeRef.getId()+","+nodeName); } QName nodeTypeQName = createQName(nodeType); boolean isContent = isSubClass(nodeTypeQName, ContentModel.TYPE_CONTENT); if (! isContent) { validateCmObject(nodeTypeQName); } Map props = new HashMap<>(1); if (nodeInfo.getProperties() != null) { // node properties - set any additional properties props = mapToNodeProperties(nodeInfo.getProperties()); } // Existing file/folder name handling boolean autoRename = Boolean.valueOf(parameters.getParameter(PARAM_AUTO_RENAME)); if (autoRename && (isContent || isSubClass(nodeTypeQName, ContentModel.TYPE_FOLDER))) { NodeRef existingNode = nodeService.getChildByName(parentNodeRef, ContentModel.ASSOC_CONTAINS, nodeName); if (existingNode != null) { // File already exists, find a unique name nodeName = findUniqueName(parentNodeRef, nodeName); } } String relativePath = nodeInfo.getRelativePath(); parentNodeRef = getOrCreatePath(parentNodeRef, relativePath); // Create the node NodeRef nodeRef = createNodeImpl(parentNodeRef, nodeName, nodeTypeQName, props); List aspectNames = nodeInfo.getAspectNames(); if (aspectNames != null) { // node aspects - set any additional aspects Set aspectQNames = mapToNodeAspects(aspectNames); for (QName aspectQName : aspectQNames) { if (EXCLUDED_ASPECTS.contains(aspectQName) || aspectQName.equals(ContentModel.ASPECT_AUDITABLE)) { continue; // ignore } nodeService.addAspect(nodeRef, aspectQName, null); } } if (isContent) { // add empty file ContentWriter writer = contentService.getWriter(nodeRef, ContentModel.PROP_CONTENT, true); setWriterContentType(writer, new ContentInfoWrapper(nodeInfo.getContent()), nodeRef, false); writer.putContent(""); } return getFolderOrDocument(nodeRef.getId(), parameters); } private NodeRef getOrCreatePath(NodeRef parentNodeRef, String relativePath) { if (relativePath != null) { List pathElements = getPathElements(relativePath); // Checks for the presence of, and creates as necessary, // the folder structure in the provided path elements list. if (pathElements != null && !pathElements.isEmpty()) { parentNodeRef = makeFolders(parentNodeRef, pathElements); } } return parentNodeRef; } private NodeRef createNodeImpl(NodeRef parentNodeRef, String nodeName, QName nodeTypeQName, Map props) { if (props == null) { props = new HashMap<>(1); } props.put(ContentModel.PROP_NAME, nodeName); validatePropValues(props); QName assocQName = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, QName.createValidLocalName(nodeName)); try { return nodeService.createNode(parentNodeRef, ContentModel.ASSOC_CONTAINS, assocQName, nodeTypeQName, props).getChildRef(); } catch (DuplicateChildNodeNameException dcne) { // duplicate - name clash throw new ConstraintViolatedException(dcne.getMessage()); } } // check cm:cmobject (but *not* cm:systemfolder) private void validateCmObject(QName nodeTypeQName) { if (! isSubClass(nodeTypeQName, ContentModel.TYPE_CMOBJECT)) { throw new InvalidArgumentException("Invalid type: " + nodeTypeQName + " - expected (sub-)type of cm:cmobject"); } if (isSubClass(nodeTypeQName, ContentModel.TYPE_SYSTEM_FOLDER)) { throw new InvalidArgumentException("Invalid type: " + nodeTypeQName + " - cannot be (sub-)type of cm:systemfolder"); } } // special cases: additional validation of property values (if not done by underlying foundation services) private void validatePropValues(Map props) { String newOwner = (String)props.get(ContentModel.PROP_OWNER); if (newOwner != null) { // validate that user exists if (! personService.personExists(newOwner)) { throw new InvalidArgumentException("Unknown owner: "+newOwner); } } } // special case: additional delete validation (pending common lower-level service support) // for blacklist of system nodes that should not be deleted, eg. Company Home, Sites, Data Dictionary private boolean isSpecialNodeDoNotDelete(NodeRef nodeRef, QName type) { // Check for Company Home, Sites and Data Dictionary (note: must be tenant-aware) if (nodeRef.equals(repositoryHelper.getCompanyHome())) { return true; } else if (type.equals(SiteModel.TYPE_SITES) || type.equals(SiteModel.TYPE_SITE)) { // note: alternatively, we could inject SiteServiceInternal and use getSitesRoot (or indirectly via node locator) return true; } else { String tenantDomain = TenantUtil.getCurrentDomain(); NodeRef ddNodeRef = ddCache.get(tenantDomain); if (ddNodeRef == null) { List ddAssocs = nodeService.getChildAssocs( repositoryHelper.getCompanyHome(), ContentModel.ASSOC_CONTAINS, QName.createQName(NamespaceService.APP_MODEL_1_0_URI, "dictionary")); if (ddAssocs.size() == 1) { ddNodeRef = ddAssocs.get(0).getChildRef(); ddCache.put(tenantDomain, ddNodeRef); } } if (nodeRef.equals(ddNodeRef)) { return true; } } return false; } @Override public Node updateNode(String nodeId, Node nodeInfo, Parameters parameters) { final NodeRef nodeRef = validateNode(nodeId); QName nodeTypeQName = getNodeType(nodeRef); validateCmObject(nodeTypeQName); Map props = new HashMap<>(0); if (nodeInfo.getProperties() != null) { props = mapToNodeProperties(nodeInfo.getProperties()); } String name = nodeInfo.getName(); if ((name != null) && (! name.isEmpty())) { // update node name if needed - note: if the name is different than existing then this is equivalent of a rename (within parent folder) props.put(ContentModel.PROP_NAME, name); } String nodeType = nodeInfo.getNodeType(); if ((nodeType != null) && (! nodeType.isEmpty())) { // update node type - ensure that we are performing a specialise (we do not support generalise) QName destNodeTypeQName = createQName(nodeType); if ((! destNodeTypeQName.equals(nodeTypeQName)) && isSubClass(destNodeTypeQName, nodeTypeQName) && (! isSubClass(destNodeTypeQName, ContentModel.TYPE_SYSTEM_FOLDER))) { nodeService.setType(nodeRef, destNodeTypeQName); } else { throw new InvalidArgumentException("Failed to change (specialise) node type - from "+nodeTypeQName+" to "+destNodeTypeQName); } } NodeRef parentNodeRef = nodeInfo.getParentId(); if (parentNodeRef != null) { NodeRef currentParentNodeRef = getParentNodeRef(nodeRef); if (currentParentNodeRef == null) { // implies root (Company Home) hence return 403 here throw new PermissionDeniedException(); } if (! currentParentNodeRef.equals(parentNodeRef)) { //moveOrCopy(nodeRef, parentNodeRef, name, false); // not currently supported - client should use explicit POST /move operation instead throw new InvalidArgumentException("Cannot update parentId of "+nodeId+" via PUT /nodes/{nodeId}. Please use explicit POST /nodes/{nodeId}/move operation instead"); } } List aspectNames = nodeInfo.getAspectNames(); if (aspectNames != null) { // update aspects - note: can be empty (eg. to remove existing aspects+properties) but not cm:auditable, sys:referencable, sys:localized Set aspectQNames = mapToNodeAspects(aspectNames); Set existingAspects = nodeService.getAspects(nodeRef); Set aspectsToAdd = new HashSet<>(3); Set aspectsToRemove = new HashSet<>(3); for (QName aspectQName : aspectQNames) { if (EXCLUDED_NS.contains(aspectQName.getNamespaceURI()) || EXCLUDED_ASPECTS.contains(aspectQName) || aspectQName.equals(ContentModel.ASPECT_AUDITABLE)) { continue; // ignore } if (! existingAspects.contains(aspectQName)) { aspectsToAdd.add(aspectQName); } } for (QName existingAspect : existingAspects) { if (EXCLUDED_NS.contains(existingAspect.getNamespaceURI()) || EXCLUDED_ASPECTS.contains(existingAspect) || existingAspect.equals(ContentModel.ASPECT_AUDITABLE)) { continue; // ignore } if (! aspectQNames.contains(existingAspect)) { aspectsToRemove.add(existingAspect); } } // Note: for now, if aspectNames are sent then all that are required should be sent (to avoid properties from other existing aspects being removed) // TODO: optional PATCH mechanism to add one new new aspect (with some related aspect properties) without affecting existing aspects/properties for (QName aQName : aspectsToRemove) { if (aQName.equals(QuickShareModel.ASPECT_QSHARE)) { String qSharedId = (String)nodeService.getProperty(nodeRef, QuickShareModel.PROP_QSHARE_SHAREDID); if (qSharedId != null) { // note: for now, go via QuickShareLinks (rather than QuickShareService) to ensure consistent permission checks // alternatively we could disallow (or ignore) "qshare:shared" aspect removal quickShareLinks.delete(qSharedId, null); } } nodeService.removeAspect(nodeRef, aQName); } for (QName aQName : aspectsToAdd) { if (aQName.equals(QuickShareModel.ASPECT_QSHARE)) { // note: for now, go via QuickShareLinks (rather than QuickShareService) to ensure consistent permission checks // alternatively we could disallow (or ignore) "qshare:shared" aspect addition QuickShareLink qs = new QuickShareLink(); qs.setNodeId(nodeRef.getId()); quickShareLinks.create(Collections.singletonList(qs), null); } nodeService.addAspect(nodeRef, aQName, null); } } if (props.size() > 0) { validatePropValues(props); try { // update node properties - note: null will unset the specified property nodeService.addProperties(nodeRef, props); } catch (DuplicateChildNodeNameException dcne) { throw new ConstraintViolatedException(dcne.getMessage()); } } return getFolderOrDocument(nodeRef.getId(), parameters); } @Override public Node moveOrCopyNode(String sourceNodeId, String targetParentId, String name, Parameters parameters, boolean isCopy) { if ((sourceNodeId == null) || (sourceNodeId.isEmpty())) { throw new InvalidArgumentException("Missing sourceNodeId"); } if ((targetParentId == null) || (targetParentId.isEmpty())) { throw new InvalidArgumentException("Missing targetParentId"); } final NodeRef parentNodeRef = validateOrLookupNode(targetParentId, null); final NodeRef sourceNodeRef = validateOrLookupNode(sourceNodeId, null); FileInfo fi = moveOrCopyImpl(sourceNodeRef, parentNodeRef, name, isCopy); return getFolderOrDocument(fi.getNodeRef().getId(), parameters); } protected FileInfo moveOrCopyImpl(NodeRef nodeRef, NodeRef parentNodeRef, String name, boolean isCopy) { String targetParentId = parentNodeRef.getId(); try { if (isCopy) { // copy return fileFolderService.copy(nodeRef, parentNodeRef, name); } else { // move if ((! nodeRef.equals(parentNodeRef)) && isSpecialNodeDoNotDelete(nodeRef, getNodeType(nodeRef))) { throw new PermissionDeniedException("Cannot move: "+nodeRef.getId()); } // updating "parentId" means moving primary parent ! // note: in the future (as and when we support secondary parent/child assocs) we may also // wish to select which parent to "move from" (in case where the node resides in multiple locations) return fileFolderService.move(nodeRef, parentNodeRef, name); } } catch (InvalidNodeRefException inre) { throw new EntityNotFoundException(targetParentId); } catch (FileNotFoundException fnfe) { // convert checked exception throw new EntityNotFoundException(targetParentId); } catch (FileExistsException fee) { // duplicate - name clash throw new ConstraintViolatedException("Name already exists in target parent: "+name); } catch (FileFolderServiceImpl.InvalidTypeException ite) { throw new InvalidArgumentException("Invalid type of target parent: "+targetParentId); } catch (CyclicChildRelationshipException ccre) { throw new InvalidArgumentException("Parent/child cycle detected: "+targetParentId); } } @Override public BinaryResource getContent(String fileNodeId, Parameters parameters) { final NodeRef nodeRef = validateNode(fileNodeId); if (! nodeMatches(nodeRef, Collections.singleton(ContentModel.TYPE_CONTENT), null, false)) { throw new InvalidArgumentException("NodeId of content is expected: "+nodeRef.getId()); } Map nodeProps = nodeService.getProperties(nodeRef); ContentData cd = (ContentData)nodeProps.get(ContentModel.PROP_CONTENT); String name = (String)nodeProps.get(ContentModel.PROP_NAME); org.alfresco.rest.framework.resource.content.ContentInfo ci = null; String mimeType = null; if (cd != null) { mimeType = cd.getMimetype(); ci = new org.alfresco.rest.framework.resource.content.ContentInfoImpl(mimeType, cd.getEncoding(), cd.getSize(), cd.getLocale()); } // By default set attachment header (with filename) unless attachment=false *and* content type is pre-configured as non-attach boolean attach = true; String attachment = parameters.getParameter("attachment"); if (attachment != null) { Boolean a = Boolean.valueOf(attachment); if (!a) { if (nonAttachContentTypes.contains(mimeType)) { attach = false; } else { logger.warn("Ignored attachment=false for "+fileNodeId+" since "+mimeType+" is not in the whitelist for non-attach content types"); } } } String attachFileName = (attach ? name : null); return new NodeBinaryResource(nodeRef, ContentModel.PROP_CONTENT, ci, attachFileName); } @Override public Node updateContent(String fileNodeId, BasicContentInfo contentInfo, InputStream stream, Parameters parameters) { final NodeRef nodeRef = validateNode(fileNodeId); if (! nodeMatches(nodeRef, Collections.singleton(ContentModel.TYPE_CONTENT), null, false)) { throw new InvalidArgumentException("NodeId of content is expected: " + nodeRef.getId()); } Boolean versionMajor = null; String str = parameters.getParameter(PARAM_VERSION_MAJOR); if (str != null) { versionMajor = new Boolean(str); } String versionComment = parameters.getParameter(PARAM_VERSION_COMMENT); return updateExistingFile(nodeRef, contentInfo, stream, parameters, versionMajor, versionComment); } private Node updateExistingFile(NodeRef nodeRef, BasicContentInfo contentInfo, InputStream stream, Parameters parameters, Boolean versionMajor, String versionComment) { boolean isVersioned = versionService.isVersioned(nodeRef); behaviourFilter.disableBehaviour(nodeRef, ContentModel.ASPECT_VERSIONABLE); try { writeContent(nodeRef, contentInfo, stream); if ((isVersioned) || (versionMajor != null) || (versionComment != null) ) { VersionType versionType = VersionType.MINOR; if ((versionMajor != null) && (versionMajor == true)) { versionType = VersionType.MAJOR; } createVersion(nodeRef, isVersioned, versionType, versionComment); } extractMetadata(nodeRef); } finally { behaviourFilter.enableBehaviour(nodeRef, ContentModel.ASPECT_VERSIONABLE); } return getFolderOrDocumentFullInfo(nodeRef, null, null, parameters); } private void writeContent(NodeRef nodeRef, BasicContentInfo contentInfo, InputStream stream) { ContentWriter writer = contentService.getWriter(nodeRef, ContentModel.PROP_CONTENT, true); setWriterContentType(writer, new ContentInfoWrapper(contentInfo), nodeRef, true); writer.putContent(stream); } protected void createVersion(NodeRef nodeRef, boolean isVersioned, VersionType versionType, String reason) { if (! isVersioned) { // Ensure the file is versionable (autoVersion = true, autoVersionProps = false) ensureVersioningEnabled(nodeRef, true, false); } else { Map versionProperties = new HashMap<>(2); versionProperties.put(VersionModel.PROP_VERSION_TYPE, versionType); if (reason != null) { versionProperties.put(VersionModel.PROP_DESCRIPTION, reason); } versionService.createVersion(nodeRef, versionProperties); } } private void setWriterContentType(ContentWriter writer, ContentInfoWrapper contentInfo, NodeRef nodeRef, boolean guessEncodingIfNull) { String mimeType = contentInfo.mimeType; // Manage MimeType if ((mimeType == null) || mimeType.equals(DEFAULT_MIMETYPE)) { // the mimeType was not provided (or was the default binary mimeType) via the contentInfo, so try to guess final String fileName = (String) nodeService.getProperty(nodeRef, ContentModel.PROP_NAME); mimeType = mimetypeService.guessMimetype(fileName); } writer.setMimetype(mimeType); // Manage Encoding if (contentInfo.encoding == null) { if (guessEncodingIfNull) { // the encoding was not provided, so try to guess writer.guessEncoding(); } } else { writer.setEncoding(contentInfo.encoding); } } @Override public Node upload(String parentFolderNodeId, FormData formData, Parameters parameters) { if (formData == null || !formData.getIsMultiPart()) { throw new InvalidArgumentException("The request content-type is not multipart: "+parentFolderNodeId); } NodeRef parentNodeRef = validateOrLookupNode(parentFolderNodeId, null); if (!nodeMatches(parentNodeRef, Collections.singleton(ContentModel.TYPE_FOLDER), null, false)) { throw new InvalidArgumentException("NodeId of folder is expected: " + parentNodeRef.getId()); } String fileName = null; Content content = null; boolean autoRename = false; QName nodeTypeQName = null; boolean overwrite = false; // If a fileName clashes for a versionable file Boolean majorVersion = null; String versionComment = null; String relativePath = null; Map qnameStrProps = new HashMap<>(); Map properties = null; for (FormData.FormField field : formData.getFields()) { switch (field.getName().toLowerCase()) { case "filename": String str = getStringOrNull(field.getValue()); if ((str != null) && (! str.isEmpty())) { fileName = str; } break; case "filedata": if (field.getIsFile()) { fileName = (fileName != null ? fileName : field.getFilename()); content = field.getContent(); } break; case "autorename": autoRename = Boolean.valueOf(field.getValue()); break; case "nodetype": nodeTypeQName = createQName(getStringOrNull(field.getValue())); if (! isSubClass(nodeTypeQName, ContentModel.TYPE_CONTENT)) { throw new InvalidArgumentException("Can only upload type of cm:content: " + nodeTypeQName); } break; case "overwrite": overwrite = Boolean.valueOf(field.getValue()); break; case "majorversion": majorVersion = Boolean.valueOf(field.getValue()); break; case "comment": versionComment = getStringOrNull(field.getValue()); break; case "relativepath": relativePath = getStringOrNull(field.getValue()); break; default: { final String propName = field.getName(); if (propName.indexOf(QName.NAMESPACE_PREFIX) > -1) { qnameStrProps.put(propName, field.getValue()); } } } } // MNT-7213 When alf_data runs out of disk space, Share uploads // result in a success message, but the files do not appear. if (formData.getFields().length == 0) { throw new ConstraintViolatedException("No disk space available"); } // Ensure mandatory file attributes have been located. Need either // destination, or site + container or updateNodeRef if ((fileName == null) || fileName.isEmpty() || (content == null)) { throw new InvalidArgumentException("Required parameters are missing"); } if (autoRename && overwrite) { throw new InvalidArgumentException("Both 'overwrite' and 'autoRename' should not be true when uploading a file"); } // if requested, make (get or create) path parentNodeRef = getOrCreatePath(parentNodeRef, relativePath); try { // Map the given properties, if any. if (qnameStrProps.size() > 0) { properties = mapToNodeProperties(qnameStrProps); } /* * Existing file handling */ NodeRef existingFile = nodeService.getChildByName(parentNodeRef, ContentModel.ASSOC_CONTAINS, fileName); if (existingFile != null) { // File already exists, decide what to do if (autoRename) { // attempt to find a unique name fileName = findUniqueName(parentNodeRef, fileName); // drop-through ! } else if (overwrite && nodeService.hasAspect(existingFile, ContentModel.ASPECT_VERSIONABLE)) { // overwrite existing (versionable) file BasicContentInfo contentInfo = new ContentInfoImpl(content.getMimetype(), content.getEncoding(), -1, null); return updateExistingFile(existingFile, contentInfo, content.getInputStream(), parameters, majorVersion, versionComment); } else { // name clash (and no autoRename or overwrite) throw new ConstraintViolatedException(fileName + " already exists."); } } // Create a new file. return createNewFile(parentNodeRef, fileName, nodeTypeQName, content, properties, parameters); // Do not clean formData temp files to allow for retries. // Temp files will be deleted later when GC call DiskFileItem#finalize() method or by temp file cleaner. } catch (ApiException apiEx) { // As this is an public API fwk exception, there is no need to convert it, so just throw it. throw apiEx; } catch (AccessDeniedException ade) { throw new PermissionDeniedException(ade.getMessage()); } catch (ContentQuotaException cqe) { throw new InsufficientStorageException(); } catch (ContentLimitViolationException clv) { throw new RequestEntityTooLargeException(clv.getMessage()); } catch (Exception ex) { /* * NOTE: Do not clean formData temp files to allow for retries. It's * possible for a temp file to remain if max retry attempts are * made, but this is rare, so leave to usual temp file cleanup. */ throw new ApiException("Unexpected error occurred during upload of new content.", ex); } } private Node createNewFile(NodeRef parentNodeRef, String fileName, QName nodeType, Content content, Map props, Parameters params) { if (nodeType == null) { nodeType = ContentModel.TYPE_CONTENT; } NodeRef newFile = createNodeImpl(parentNodeRef, fileName, nodeType, props); // Write content write(newFile, content); // Ensure the file is versionable (autoVersion = true, autoVersionProps = false) ensureVersioningEnabled(newFile, true, false); // Extract the metadata extractMetadata(newFile); // Create the response return getFolderOrDocumentFullInfo(newFile, parentNodeRef, nodeType, params); } private String getStringOrNull(String value) { if (StringUtils.isNotEmpty(value)) { return value.equalsIgnoreCase("null") ? null : value; } return null; } /** * Writes the content to the repository. * * @param nodeRef the reference to the node having a content property * @param content the content */ protected void write(NodeRef nodeRef, Content content) { ContentWriter writer = contentService.getWriter(nodeRef, ContentModel.PROP_CONTENT, true); // Per RA-637 & RA-885 requirement the mimeType provided by the client takes precedence, however, // if the mimeType is null (or default binary mimeType) then it will be guessed. setWriterContentType(writer, new ContentInfoWrapper(content), nodeRef, true); writer.putContent(content.getInputStream()); } /** * Ensures the given node has the {@code cm:versionable} aspect applied to it, and * that it has the initial version in the version store. * * @param nodeRef the reference to the node to be checked * @param autoVersion If the {@code cm:versionable} aspect is applied, should auto * versioning be requested? * @param autoVersionProps If the {@code cm:versionable} aspect is applied, should * auto versioning of properties be requested? */ protected void ensureVersioningEnabled(NodeRef nodeRef, boolean autoVersion, boolean autoVersionProps) { Map props = new HashMap<>(2); props.put(ContentModel.PROP_AUTO_VERSION, autoVersion); props.put(ContentModel.PROP_AUTO_VERSION_PROPS, autoVersionProps); versionService.ensureVersioningEnabled(nodeRef, props); } /** * Extracts the given node metadata asynchronously. * * The overwrite policy controls which if any parts of the document's properties are updated from this. */ private void extractMetadata(NodeRef nodeRef) { final String actionName = ContentMetadataExtracter.EXECUTOR_NAME; ActionDefinition actionDef = actionService.getActionDefinition(actionName); if (actionDef != null) { Action action = actionService.createAction(actionName); actionService.executeAction(action, nodeRef); } } /** * Creates a unique file name, if the upload component was configured to * find a new unique name for clashing filenames. * * @param parentNodeRef the parent node * @param fileName the original fileName * @return a new file name */ private String findUniqueName(NodeRef parentNodeRef, String fileName) { int counter = 1; String tmpFilename; NodeRef existingFile; do { int dotIndex = fileName.lastIndexOf('.'); if (dotIndex == 0) { // File didn't have a proper 'name' instead it // had just a suffix and started with a ".", create "1.txt" tmpFilename = counter + fileName; } else if (dotIndex > 0) { // Filename contained ".", create "fileName-1.txt" tmpFilename = fileName.substring(0, dotIndex) + "-" + counter + fileName.substring(dotIndex); } else { // Filename didn't contain a dot at all, create "fileName-1" tmpFilename = fileName + "-" + counter; } existingFile = nodeService.getChildByName(parentNodeRef, ContentModel.ASSOC_CONTAINS, tmpFilename); counter++; } while (existingFile != null); return tmpFilename; } /** * Helper to create a QName from either a fully qualified or short-name QName string * * @param qnameStr Fully qualified or short-name QName string * @return QName */ protected QName createQName(String qnameStr) { try { QName qname; if (qnameStr.indexOf(QName.NAMESPACE_BEGIN) != -1) { qname = QName.createQName(qnameStr); } else { qname = QName.createQName(qnameStr, namespaceService); } return qname; } catch (Exception ex) { String msg = ex.getMessage(); if (msg == null) { msg = ""; } throw new InvalidArgumentException(qnameStr + " isn't a valid QName. " + msg); } } /** * Helper to create a QName from either a fully qualified or short-name QName string * * @param qnameStrList list of fully qualified or short-name QName string * @return a list of {@code QName} objects */ protected List createQNames(List qnameStrList) { String PREFIX = PARAM_INCLUDE_PROPERTIES +"/"; List result = new ArrayList<>(qnameStrList.size()); for (String str : qnameStrList) { if (str.startsWith(PREFIX)) { str = str.substring(PREFIX.length()); } QName name = createQName(str); if (!EXCLUDED_PROPS.contains(name)) { result.add(name); } } return result; } /** * @author Jamal Kaabi-Mofrad */ private static class ContentInfoWrapper { private String mimeType; private String encoding; ContentInfoWrapper(BasicContentInfo basicContentInfo) { if (basicContentInfo != null) { this.mimeType = basicContentInfo.getMimeType(); this.encoding = basicContentInfo.getEncoding(); } } ContentInfoWrapper(ContentInfo contentInfo) { if (contentInfo != null) { this.mimeType = contentInfo.getMimeType(); this.encoding = contentInfo.getEncoding(); } } ContentInfoWrapper(Content content) { if (content != null && StringUtils.isNotEmpty(content.getMimetype())) { try { // TODO I think it makes sense to push contentType parsing into org.springframework.extensions.webscripts.servlet.FormData MediaType media = MediaType.parseMediaType(content.getMimetype()); this.mimeType = media.getType() + '/' + media.getSubtype(); if (media.getCharSet() != null) { this.encoding = media.getCharSet().name(); } } catch (InvalidMediaTypeException ime) { throw new InvalidArgumentException(ime.getMessage()); } } } } }