/*
* #%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 org.alfresco.model.ApplicationModel;
import org.alfresco.model.ContentModel;
import org.alfresco.query.PagingRequest;
import org.alfresco.query.PagingResults;
import org.alfresco.repo.content.ContentLimitViolationException;
import org.alfresco.repo.model.Repository;
import org.alfresco.repo.node.getchildren.GetChildrenCannedQuery;
import org.alfresco.repo.security.permissions.AccessDeniedException;
import org.alfresco.rest.antlr.WhereClauseParser;
import org.alfresco.rest.api.Nodes;
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.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.InvalidArgumentException;
import org.alfresco.rest.framework.core.exceptions.NotFoundException;
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.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.DataTypeDefinition;
import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.dictionary.PropertyDefinition;
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.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.PermissionService;
import org.alfresco.service.cmr.usage.ContentQuotaException;
import org.alfresco.service.cmr.version.VersionService;
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.extensions.surf.util.Content;
import org.springframework.extensions.webscripts.servlet.FormData;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.math.BigInteger;
import java.nio.charset.Charset;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Arrays;
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;
/**
* Centralises access to file/folder/node services and maps between representations.
*
* @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 static enum Type
{
// Note: ordered
DOCUMENT, FOLDER;
};
private final static String PARAM_RELATIVE_PATH = "relativePath"; // TODO wip
private final static String PARAM_SELECT_PROPERTIES = "properties";
private final static String PARAM_SELECT_PATH = "path";
private final static String PARAM_SELECT_ASPECTNAMES = "aspectNames";
private final static String PARAM_SELECT_ISLINK = "isLink";
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 Repository repositoryHelper;
private ServiceRegistry sr;
private Set defaultIgnoreTypes;
private Set ignoreTypeQNames;
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();
if (defaultIgnoreTypes != null)
{
ignoreTypeQNames = new HashSet<>(defaultIgnoreTypes.size());
for (String type : defaultIgnoreTypes)
{
ignoreTypeQNames.add(createQName(type));
}
}
}
public void setServiceRegistry(ServiceRegistry sr) {
this.sr = sr;
}
public void setRepositoryHelper(Repository repositoryHelper)
{
this.repositoryHelper = repositoryHelper;
}
public void setIgnoreTypes(Set ignoreTypes)
{
this.defaultIgnoreTypes = ignoreTypes;
}
private static final List EXCLUDED_ASPECTS = Arrays.asList(
ContentModel.ASPECT_REFERENCEABLE,
ContentModel.ASPECT_LOCALIZED);
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,
// sys:localized
ContentModel.PROP_LOCALE,
// sys:referenceable
ContentModel.PROP_NODE_UUID,
ContentModel.PROP_STORE_IDENTIFIER,
ContentModel.PROP_STORE_PROTOCOL,
ContentModel.PROP_NODE_DBID,
// 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 String PARAM_ISFOLDER = "isFolder";
private final static String PARAM_NAME = "name";
private final static String PARAM_CREATEDAT = "createdAt";
private final static String PARAM_MODIFIEDAT = "modifiedAt";
private final static String PARAM_CREATEBYUSER = "createdByUser";
private final static String PARAM_MODIFIEDBYUSER = "modifiedByUser";
private final static String PARAM_MIMETYPE = "mimeType";
private final static String PARAM_SIZEINBYTES = "sizeInBytes";
private final static String PARAM_NODETYPE = "nodeType";
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);
}
private final static Set LIST_FOLDER_CHILDREN_EQUALS_QUERY_PROPERTIES =
new HashSet<>(Arrays.asList(new String[] {PARAM_ISFOLDER}));
/*
* Note: assumes workspace://SpacesStore
*/
public NodeRef validateNode(String nodeId)
{
return validateNode(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, nodeId);
}
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);
}
public NodeRef validateNode(NodeRef nodeRef)
{
if (!nodeService.exists(nodeRef))
{
throw new EntityNotFoundException(nodeRef.getId());
}
return nodeRef;
}
public boolean nodeMatches(NodeRef nodeRef, Set expectedTypes, Set excludedTypes)
{
if (!nodeService.exists(nodeRef))
{
throw new EntityNotFoundException(nodeRef.getId());
}
return typeMatches(nodeService.getType(nodeRef), expectedTypes, excludedTypes);
}
protected boolean typeMatches(QName type, Set expectedTypes, Set excludedTypes)
{
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')
*/
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(nodeService.getType(nodeRef), nodeRef);
}
private Type getType(QName typeQName, NodeRef nodeRef)
{
if (dictionaryService.isSubClass(typeQName, ContentModel.TYPE_LINK))
{
// handle file/folder link type (we do not explicitly validate that the destination type matches)
if (dictionaryService.isSubClass(typeQName, ApplicationModel.TYPE_FOLDERLINK))
{
return Type.FOLDER;
}
else if (dictionaryService.isSubClass(typeQName, ApplicationModel.TYPE_FILELINK))
{
return Type.DOCUMENT;
}
else
{
// cm:link (or other subclass)
NodeRef linkNodeRef = (NodeRef)nodeService.getProperty(nodeRef, ContentModel.PROP_LINK_DESTINATION);
if (linkNodeRef != null)
{
try
{
typeQName = nodeService.getType(linkNodeRef);
}
catch (InvalidNodeRefException inre)
{
// ignore
}
}
}
}
boolean isContainer = (dictionaryService.isSubClass(typeQName, ContentModel.TYPE_FOLDER) &&
(! dictionaryService.isSubClass(typeQName, ContentModel.TYPE_SYSTEM_FOLDER)));
return isContainer ? Type.FOLDER : Type.DOCUMENT;
}
/**
* @deprecated note: currently required for backwards compat' (Favourites API)
*/
public Document getDocument(NodeRef nodeRef)
{
Type type = getType(nodeRef);
if (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");
}
}
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)
*/
public Folder getFolder(NodeRef nodeRef)
{
Type type = getType(nodeRef);
if (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");
}
}
private NodeRef getParentNodeRef(final 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();
}
private NodeRef validateOrLookupNode(String nodeId, String path)
{
NodeRef parentNodeRef;
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)
{
// 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 = new ArrayList<>(0);
if ((path != null) && (! path.isEmpty())) {
if (path.startsWith("/")) {
path = path.substring(1);
}
if (! path.isEmpty()) {
pathElements.addAll(Arrays.asList(path.split("/")));
if (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.size() != 0) {
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 InvalidNodeRefException(fnfe.getMessage()+" ["+path+"]", parentNodeRef);
}
return fileInfo.getNodeRef();
}
public Node getFolderOrDocument(String nodeId, Parameters parameters)
{
String path = parameters.getParameter(PARAM_RELATIVE_PATH);
NodeRef nodeRef = validateOrLookupNode(nodeId, path);
QName typeQName = nodeService.getType(nodeRef);
List selectParam = new ArrayList<>();
selectParam.addAll(parameters.getSelectedProperties());
// Add basic info for single get (above & beyond minimal that is used for listing collections)
selectParam.add(PARAM_SELECT_ASPECTNAMES);
selectParam.add(PARAM_SELECT_PROPERTIES);
return getFolderOrDocument(nodeRef, getParentNodeRef(nodeRef), typeQName, selectParam, null);
}
private Node getFolderOrDocument(final NodeRef nodeRef, NodeRef parentNodeRef, QName nodeTypeQName, List selectParam, Map mapUserInfo)
{
if (mapUserInfo == null) {
mapUserInfo = new HashMap<>(2);
}
PathInfo pathInfo = null;
if (selectParam.contains(PARAM_SELECT_PATH))
{
pathInfo = lookupPathInfo(nodeRef);
}
Type type = getType(nodeTypeQName, nodeRef);
Node node;
Map properties = nodeService.getProperties(nodeRef);
if (type.equals(Type.DOCUMENT))
{
node = new Document(nodeRef, parentNodeRef, properties, mapUserInfo, sr);
}
else if (type.equals(Type.FOLDER))
{
// container/folder
node = new Folder(nodeRef, parentNodeRef, properties, mapUserInfo, sr);
}
else
{
throw new InvalidArgumentException("Node is not a folder or file");
}
if (selectParam.size() > 0)
{
node.setProperties(mapFromNodeProperties(properties, selectParam, mapUserInfo));
}
if (selectParam.contains(PARAM_SELECT_ASPECTNAMES))
{
node.setAspectNames(mapFromNodeAspects(nodeService.getAspects(nodeRef)));
}
if (selectParam.contains(PARAM_SELECT_ISLINK))
{
boolean isLink = typeMatches(nodeTypeQName, Collections.singleton(ContentModel.TYPE_LINK), null);
node.setIsLink(isLink);
}
node.setNodeType(nodeTypeQName.toPrefixString(namespaceService));
node.setPath(pathInfo);
return node;
}
protected PathInfo lookupPathInfo(NodeRef nodeRefIn)
{
// TODO which implementation?
return getPathInfo(nodeRefIn);
// List elements = new ArrayList<>(5);
//
// NodeRef companyHomeNodeRef = repositoryHelper.getCompanyHome();
// boolean isComplete = true;
//
// NodeRef pNodeRef = nodeRefIn;
// while (pNodeRef != null)
// {
// if (pNodeRef.equals(companyHomeNodeRef))
// {
// pNodeRef = null;
// }
// else {
// pNodeRef = nodeService.getPrimaryParent(pNodeRef).getParentRef();
//
// if (pNodeRef == null)
// {
// // belts-and-braces - is it even possible to get here ?
// isComplete = false;
// }
// else
// {
// if (permissionService.hasPermission(pNodeRef, PermissionService.READ)
// == AccessStatus.ALLOWED)
// {
// String name = (String) nodeService.getProperty(pNodeRef,
// ContentModel.PROP_NAME);
// elements.add(0, new ElementInfo(pNodeRef, name));
// }
// else
// {
// isComplete = false;
// pNodeRef = null;
// }
// }
// }
// }
//
// StringBuilder sb = new StringBuilder();
// for (PathInfo.ElementInfo e : elements)
// {
// sb.append("/").append(e.getName());
// }
//
// return new PathInfo(sb.toString(), isComplete, elements);
}
private PathInfo getPathInfo(NodeRef nodeRef)
{
final Path nodePath = nodeService.getPath(nodeRef);
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, 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 Map mapToNodeProperties(Map props)
{
Map nodeProps = new HashMap<>(props.size());
for (Entry entry : props.entrySet())
{
QName propQName = QName.createQName(entry.getKey(), namespaceService);
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);
}
}
return nodeProps;
}
protected Map mapFromNodeProperties(Map nodeProps, List selectParam, Map mapUserInfo)
{
List selectedProperties;
if ((selectParam.size() == 0) || selectParam.contains(PARAM_SELECT_PROPERTIES))
{
// return all properties
selectedProperties = new ArrayList<>(nodeProps.size());
for (QName name : nodeProps.keySet())
{
if (!EXCLUDED_PROPS.contains(name))
{
selectedProperties.add(name);
}
}
}
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 aspectName : nodeAspects)
{
if (! EXCLUDED_ASPECTS.contains(aspectName))
{
aspectNames.add(aspectName.toPrefixString(namespaceService));
}
}
if (aspectNames.size() == 0)
{
aspectNames = null; // no aspects to return
}
return aspectNames;
}
public CollectionWithPagingInfo getChildren(String parentFolderNodeId, Parameters parameters)
{
final NodeRef parentNodeRef = validateOrLookupNode(parentFolderNodeId, null);
final List selectParam = parameters.getSelectedProperties();
boolean includeFolders = true;
boolean includeFiles = true;
Query q = parameters.getQuery();
if (q != null)
{
// TODO confirm list of filter props - what about custom props (+ across types/aspects) ?
MapBasedQueryWalker propertyWalker = new MapBasedQueryWalker(LIST_FOLDER_CHILDREN_EQUALS_QUERY_PROPERTIES, null);
QueryHelper.walk(q, propertyWalker);
Boolean b = propertyWalker.getProperty(PARAM_ISFOLDER, WhereClauseParser.EQUALS, Boolean.class);
if (b != null)
{
includeFiles = !b;
includeFolders = b;
}
}
List sortCols = parameters.getSorting();
List> sortProps = null;
if ((sortCols != null) && (sortCols.size() > 0))
{
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();
if (! nodeMatches(parentNodeRef, Collections.singleton(ContentModel.TYPE_FOLDER), null))
{
throw new InvalidArgumentException("NodeId of folder is expected");
}
PagingRequest pagingRequest = Util.getPagingRequest(paging);
final PagingResults pagingResults = fileFolderService.list(parentNodeRef, includeFiles, includeFolders, ignoreTypeQNames, 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(), selectParam, mapUserInfo);
}
@Override
public int size()
{
return page.size();
}
};
return CollectionWithPagingInfo.asPaged(paging, nodes, pagingResults.hasMoreItems(), pagingResults.getTotalResultCount().getFirst());
}
public void deleteNode(String nodeId)
{
NodeRef nodeRef = validateNode(nodeId);
fileFolderService.delete(nodeRef);
}
// TODO should we able to specify content properties (eg. mimeType ... or use extension for now, or encoding)
public Node createNode(String parentFolderNodeId, Node nodeInfo, Parameters parameters)
{
// check that requested parent node exists and it's type is a (sub-)type of folder
final NodeRef parentNodeRef = validateOrLookupNode(parentFolderNodeId, null);
if (! nodeMatches(parentNodeRef, Collections.singleton(ContentModel.TYPE_FOLDER), null))
{
throw new InvalidArgumentException("NodeId of folder is expected: "+parentNodeRef);
}
// node name - mandatory
String nodeName = nodeInfo.getName();
if ((nodeName == null) || nodeName.isEmpty())
{
throw new InvalidArgumentException("Node name is expected: "+parentNodeRef);
}
// node type - check that requested type is a (sub-) type of folder or content
String nodeType = nodeInfo.getNodeType();
if ((nodeType == null) || nodeType.isEmpty())
{
throw new InvalidArgumentException("Node type is expected: "+parentNodeRef+","+nodeName);
}
QName nodeTypeQName = createQName(nodeType);
Set contentAndFolders = new HashSet<>(
Arrays.asList(ContentModel.TYPE_FOLDER, ContentModel.TYPE_CONTENT, ContentModel.TYPE_LINK));
if (! typeMatches(nodeTypeQName, contentAndFolders, null))
{
throw new InvalidArgumentException("Type of cm:folder cm:content or cm:link is expected: "+ nodeType);
}
boolean isContent = typeMatches(nodeTypeQName, Collections.singleton(ContentModel.TYPE_CONTENT), null);
Map props = new HashMap<>(1);
if (nodeInfo.getProperties() != null)
{
// node properties - set any additional properties
props = mapToNodeProperties(nodeInfo.getProperties());
}
props.put(ContentModel.PROP_NAME, nodeName);
QName assocQName = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, QName.createValidLocalName(nodeName));
NodeRef nodeRef;
try
{
nodeRef = nodeService.createNode(parentNodeRef, ContentModel.ASSOC_CONTAINS, assocQName, nodeTypeQName, props).getChildRef();
}
catch (DuplicateChildNodeNameException dcne)
{
throw new ConstraintViolatedException(dcne.getMessage());
}
List aspectNames = nodeInfo.getAspectNames();
if (aspectNames != null)
{
// node aspects - set any additional aspects
for (String aspectName : aspectNames)
{
QName aspectQName = QName.createQName(aspectName, namespaceService);
if (EXCLUDED_ASPECTS.contains(aspectQName) || aspectQName.equals(ContentModel.ASPECT_AUDITABLE))
{
continue; // ignore
}
nodeService.addAspect(nodeRef, aspectQName, null);
}
}
if (isContent) {
// add empty file
ContentWriter writer = sr.getContentService().getWriter(nodeRef, ContentModel.PROP_CONTENT, true);
String mimeType = sr.getMimetypeService().guessMimetype(nodeName);
writer.setMimetype(mimeType);
writer.putContent("");
}
return getFolderOrDocument(nodeRef.getId(), parameters);
}
public Node updateNode(String nodeId, Node nodeInfo, Parameters parameters)
{
final NodeRef nodeRef = validateNode(nodeId);
QName nodeTypeQName = nodeService.getType(nodeRef);
final Set fileOrFolder = new HashSet<>(Arrays.asList(ContentModel.TYPE_FOLDER, ContentModel.TYPE_CONTENT));
if (! typeMatches(nodeTypeQName, fileOrFolder, null))
{
throw new InvalidArgumentException("NodeId of file or folder is expected");
}
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 = QName.createQName(nodeType, namespaceService);
if ((! destNodeTypeQName.equals(nodeTypeQName)) && dictionaryService.isSubClass(destNodeTypeQName, nodeTypeQName))
{
nodeService.setType(nodeRef, destNodeTypeQName);
}
else
{
throw new IllegalArgumentException("Failed to change (specialise) the node type - from "+nodeTypeQName+" to "+destNodeTypeQName);
}
}
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 = new HashSet<>(aspectNames.size());
for (String aspectName : aspectNames)
{
QName aspectQName = QName.createQName(aspectName, namespaceService);
aspectQNames.add(aspectQName);
}
Set existingAspects = nodeService.getAspects(nodeRef);
Set aspectsToAdd = new HashSet<>(3);
Set aspectsToRemove = new HashSet<>(3);
for (QName aspectQName : aspectQNames)
{
if (EXCLUDED_ASPECTS.contains(aspectQName) || aspectQName.equals(ContentModel.ASPECT_AUDITABLE))
{
continue; // ignore
}
if (! existingAspects.contains(aspectQName))
{
aspectsToAdd.add(aspectQName);
}
}
for (QName existingAspect : existingAspects)
{
if (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)
{
nodeService.removeAspect(nodeRef, aQName);
}
for (QName aQName : aspectsToAdd)
{
nodeService.addAspect(nodeRef, aQName, null);
}
}
if (props.size() > 0)
{
// update node properties - note: null will unset the specified property
nodeService.addProperties(nodeRef, props);
}
return getFolderOrDocument(nodeRef.getId(), parameters);
}
@Override
public BinaryResource getContent(String fileNodeId, Parameters parameters)
{
final NodeRef nodeRef = validateNode(fileNodeId);
if (! nodeMatches(nodeRef, Collections.singleton(ContentModel.TYPE_CONTENT), null))
{
throw new InvalidArgumentException("NodeId of content is expected: "+nodeRef);
}
// TODO attachment header - update (or extend ?) REST fwk
return new NodeBinaryResource(nodeRef, ContentModel.PROP_CONTENT);
}
@Override
public void updateContent(String fileNodeId, BasicContentInfo contentInfo, InputStream stream, Parameters parameters)
{
final NodeRef nodeRef = validateNode(fileNodeId);
if (! nodeMatches(nodeRef, Collections.singleton(ContentModel.TYPE_CONTENT), null))
{
throw new InvalidArgumentException("NodeId of content is expected: "+nodeRef);
}
ContentWriter writer = sr.getContentService().getWriter(nodeRef, ContentModel.PROP_CONTENT, true);
String mimeType = contentInfo.getMimeType();
if (mimeType == null)
{
String fileName = (String)nodeService.getProperty(nodeRef, ContentModel.PROP_CONTENT);
writer.guessMimetype(fileName);
}
else
{
writer.setMimetype(mimeType);
}
writer.guessEncoding();
writer.putContent(stream);
// TODO - hmm - we may wish to return json info !!
return;
}
@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");
}
final NodeRef parentNodeRef = validateOrLookupNode(parentFolderNodeId, null);
if (Type.DOCUMENT == getType(parentNodeRef))
{
throw new InvalidArgumentException(parentFolderNodeId + " is not a folder.");
}
String fileName = null;
Content content = null;
boolean autoRename = false;
boolean overwrite = false; // If a fileName clashes for a versionable file
for (FormData.FormField field : formData.getFields())
{
switch (field.getName().toLowerCase())
{
case "filename":
fileName = getStringOrNull(field.getValue());
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 "overwrite":
// overwrite = Boolean.valueOf(field.getValue());
// break;
}
}
try
{
// 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 || content == null))
{
throw new InvalidArgumentException("Required parameters are missing");
}
/*
* Existing file handling
*/
NodeRef existingFile = nodeService.getChildByName(parentNodeRef, ContentModel.ASSOC_CONTAINS, fileName);
if (existingFile != null)
{
// File already exists, decide what to do
if (autoRename)
{
fileName = findUniqueName(parentNodeRef, fileName);
}
// TODO uncomment when we decide on uploading a new version vs overwriting
// else if (overwrite && nodeService.hasAspect(existingFile, ContentModel.ASPECT_VERSIONABLE))
// {
// // Upload component was configured to overwrite files if name clashes
// write(existingFile, content, fileName, false, true);
//
// // Extract the metadata (The overwrite policy controls
// // which if any parts of the document's properties are updated from this)
// extractMetadata(existingFile);
//
// // 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.
// return createUploadResponse(parentNodeRef, existingFile);
// }
else
{
throw new ConstraintViolatedException(fileName + " already exists.");
}
}
// Create a new file.
return createNewFile(parentNodeRef, fileName, content);
// 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();
}
catch (ContentQuotaException cqe)
{
throw new RequestEntityTooLargeException();
}
catch (ContentLimitViolationException clv)
{
throw new ConstraintViolatedException();
}
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);
}
}
/**
* Helper to create a new node and writes its content to the repository.
*/
private Node createNewFile(NodeRef parentNodeRef, String fileName, Content content)
{
FileInfo fileInfo = fileFolderService.create(parentNodeRef, fileName, ContentModel.TYPE_CONTENT);
NodeRef newFile = fileInfo.getNodeRef();
// Write content
write(newFile, content, fileName, false, true);
// Ensure the file is versionable (autoVersion = true, autoVersionProps = false)
ensureVersioningEnabled(newFile, true, false);
// Extract the metadata
extractMetadata(newFile);
// Create the response
return createUploadResponse(parentNodeRef, newFile);
}
private Node createUploadResponse(NodeRef parentNodeRef, NodeRef newFileNodeRef)
{
return getFolderOrDocument(newFileNodeRef, parentNodeRef, ContentModel.TYPE_CONTENT, Collections.emptyList(), null);
}
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
* @param fileName the uploaded file name
* @param applyMimeType If true, apply the mimeType from the Content object,
* else leave the original mimeType
* @param guessEncoding If true, guess the encoding from the underlying
* input stream, else use encoding set in the Content object as supplied
*/
protected void write(NodeRef nodeRef, Content content, String fileName, boolean applyMimeType, boolean guessEncoding)
{
ContentWriter writer = contentService.getWriter(nodeRef, ContentModel.PROP_CONTENT, true);
InputStream is;
String mimeType = content.getMimetype();
if (!applyMimeType)
{
ContentData existingContentData = (ContentData) nodeService.getProperty(nodeRef, ContentModel.PROP_CONTENT);
if (existingContentData != null)
{
mimeType = existingContentData.getMimetype();
}
else
{
mimeType = mimetypeService.guessMimetype(fileName);
}
}
if (guessEncoding)
{
is = new BufferedInputStream(content.getInputStream());
is.mark(1024);
writer.setEncoding(guessEncoding(is, mimeType));
try
{
is.reset();
}
catch (IOException e)
{
logger.error("Failed to reset input stream", e);
}
}
else
{
writer.setEncoding(content.getEncoding());
is = content.getInputStream();
}
writer.setMimetype(mimeType);
writer.putContent(is);
}
/**
* 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);
}
/**
* Guesses the character encoding of the given inputStream.
*/
protected String guessEncoding(InputStream in, String mimeType)
{
String encoding = "UTF-8";
if (in != null)
{
Charset charset = mimetypeService.getContentCharsetFinder().getCharset(in, mimeType);
encoding = charset.name();
}
return encoding;
}
/**
* Extracts the given node metadata asynchronously.
*/
private void extractMetadata(NodeRef nodeRef)
{
final String actionName = "extract-metadata";
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_SELECT_PROPERTIES+"/";
List result = new ArrayList<>(qnameStrList.size());
for (String str : qnameStrList)
{
if (str.startsWith(PREFIX)) {
str = str.substring(PREFIX.length());
}
str = str.replaceFirst("_", ":"); // FIXME remove this when we have fixed the framework.
QName name = createQName(str);
if (!EXCLUDED_PROPS.contains(name))
{
result.add(name);
}
}
return result;
}
}