/*
* #%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.ContentModel;
import org.alfresco.query.PagingResults;
import org.alfresco.repo.thumbnail.ThumbnailDefinition;
import org.alfresco.repo.thumbnail.ThumbnailHelper;
import org.alfresco.repo.thumbnail.ThumbnailRegistry;
import org.alfresco.repo.thumbnail.script.ScriptThumbnailService;
import org.alfresco.rest.antlr.WhereClauseParser;
import org.alfresco.rest.api.Nodes;
import org.alfresco.rest.api.Renditions;
import org.alfresco.rest.api.model.ContentInfo;
import org.alfresco.rest.api.model.Rendition;
import org.alfresco.rest.api.model.Rendition.RenditionStatus;
import org.alfresco.rest.framework.core.exceptions.ApiException;
import org.alfresco.rest.framework.core.exceptions.ConstraintViolatedException;
import org.alfresco.rest.framework.core.exceptions.DisabledServiceException;
import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException;
import org.alfresco.rest.framework.core.exceptions.NotFoundException;
import org.alfresco.rest.framework.resource.content.BinaryResource;
import org.alfresco.rest.framework.resource.content.CacheDirective;
import org.alfresco.rest.framework.resource.content.ContentInfoImpl;
import org.alfresco.rest.framework.resource.content.FileBinaryResource;
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.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.ActionService;
import org.alfresco.service.cmr.rendition.RenditionService;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentData;
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.thumbnail.ThumbnailService;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.PropertyCheck;
import org.alfresco.util.TempFileProvider;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.ResourceLoader;
import java.io.File;
import java.io.InputStream;
import java.io.Serializable;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
/**
* @author Jamal Kaabi-Mofrad
*/
public class RenditionsImpl implements Renditions, ResourceLoaderAware
{
private static final Log LOGGER = LogFactory.getLog(RenditionsImpl.class);
private static final Set RENDITION_STATUS_COLLECTION_EQUALS_QUERY_PROPERTIES = Collections.singleton(PARAM_STATUS);
private Nodes nodes;
private NodeService nodeService;
private ThumbnailService thumbnailService;
private ScriptThumbnailService scriptThumbnailService;
private RenditionService renditionService;
private MimetypeService mimetypeService;
private ActionService actionService;
private NamespaceService namespaceService;
private ServiceRegistry serviceRegistry;
private ResourceLoader resourceLoader;
public void setNodes(Nodes nodes)
{
this.nodes = nodes;
}
public void setThumbnailService(ThumbnailService thumbnailService)
{
this.thumbnailService = thumbnailService;
}
public void setScriptThumbnailService(ScriptThumbnailService scriptThumbnailService)
{
this.scriptThumbnailService = scriptThumbnailService;
}
public void setServiceRegistry(ServiceRegistry serviceRegistry)
{
this.serviceRegistry = serviceRegistry;
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader)
{
this.resourceLoader = resourceLoader;
}
public void init()
{
PropertyCheck.mandatory(this, "nodes", nodes);
PropertyCheck.mandatory(this, "thumbnailService", thumbnailService);
PropertyCheck.mandatory(this, "scriptThumbnailService", scriptThumbnailService);
PropertyCheck.mandatory(this, "serviceRegistry", serviceRegistry);
this.nodeService = serviceRegistry.getNodeService();
this.actionService = serviceRegistry.getActionService();
this.renditionService = serviceRegistry.getRenditionService();
this.mimetypeService = serviceRegistry.getMimetypeService();
this.namespaceService = serviceRegistry.getNamespaceService();
}
@Override
public CollectionWithPagingInfo getRenditions(String nodeId, Parameters parameters)
{
final NodeRef nodeRef = validateSourceNode(nodeId);
String contentMimeType = getMimeType(nodeRef);
Query query = parameters.getQuery();
boolean includeCreated = true;
boolean includeNotCreated = true;
if (query != null)
{
// Filtering via "where" clause
MapBasedQueryWalker propertyWalker = new MapBasedQueryWalker(RENDITION_STATUS_COLLECTION_EQUALS_QUERY_PROPERTIES, null);
QueryHelper.walk(query, propertyWalker);
String withStatus = propertyWalker.getProperty(PARAM_STATUS, WhereClauseParser.EQUALS);
if (withStatus != null)
{
try
{
includeCreated = RenditionStatus.CREATED.equals(RenditionStatus.valueOf(withStatus));
}
catch (IllegalArgumentException ex)
{
throw new InvalidArgumentException("Invalid status value: " + withStatus);
}
includeNotCreated = !includeCreated;
}
}
Map apiRenditions = new TreeMap<>();
if (includeNotCreated)
{
// List all available thumbnail definitions
List thumbnailDefinitions = thumbnailService.getThumbnailRegistry().getThumbnailDefinitions(contentMimeType, -1);
for (ThumbnailDefinition thumbnailDefinition : thumbnailDefinitions)
{
apiRenditions.put(thumbnailDefinition.getName(), toApiRendition(thumbnailDefinition));
}
}
List nodeRefRenditions = renditionService.getRenditions(nodeRef);
if (!nodeRefRenditions.isEmpty())
{
for (ChildAssociationRef childAssociationRef : nodeRefRenditions)
{
NodeRef renditionNodeRef = childAssociationRef.getChildRef();
Rendition apiRendition = toApiRendition(renditionNodeRef);
if (includeCreated)
{
// Replace/append any thumbnail definitions with created rendition info
apiRenditions.put(apiRendition.getId(), apiRendition);
}
else
{
// Remove any thumbnail definitions that has been created from the list,
// as the filter requires only the Not_Created renditions
apiRenditions.remove(apiRendition.getId());
}
}
}
// Wrap paging info, as the core service doesn't support paging
Paging paging = parameters.getPaging();
PagingResults results = Util.wrapPagingResults(paging, apiRenditions.values());
return CollectionWithPagingInfo.asPaged(paging, results.getPage(), results.hasMoreItems(), results.getTotalResultCount().getFirst());
}
@Override
public Rendition getRendition(String nodeId, String renditionId, Parameters parameters)
{
final NodeRef nodeRef = validateSourceNode(nodeId);
NodeRef renditionNodeRef = getRenditionByName(nodeRef, renditionId, parameters);
// if there is no rendition, then try to find the available/registered rendition (yet to be created).
if (renditionNodeRef == null)
{
ThumbnailDefinition thumbnailDefinition = thumbnailService.getThumbnailRegistry().getThumbnailDefinition(renditionId);
if (thumbnailDefinition == null)
{
throw new NotFoundException(renditionId + " is not registered.");
}
else
{
String contentMimeType = getMimeType(nodeRef);
// List all available thumbnail definitions for the source node
List thumbnailDefinitions = thumbnailService.getThumbnailRegistry().getThumbnailDefinitions(contentMimeType, -1);
boolean found = false;
for (ThumbnailDefinition td : thumbnailDefinitions)
{
// Check the registered renditionId is applicable for the node's mimeType
if (renditionId.equals(td.getName()))
{
found = true;
break;
}
}
if (!found)
{
throw new NotFoundException(renditionId + " is not applicable for the node's mimeType " + contentMimeType);
}
}
return toApiRendition(thumbnailDefinition);
}
return toApiRendition(renditionNodeRef);
}
@Override
public void createRendition(String nodeId, Rendition rendition, Parameters parameters)
{
// If thumbnail generation has been configured off, then don't bother.
if (!thumbnailService.getThumbnailsEnabled())
{
throw new DisabledServiceException("Thumbnail generation has been disabled.");
}
final NodeRef sourceNodeRef = validateSourceNode(nodeId);
final NodeRef renditionNodeRef = getRenditionByName(sourceNodeRef, rendition.getId(), parameters);
if (renditionNodeRef != null)
{
throw new ConstraintViolatedException(rendition.getId() + " rendition already exists.");
}
// Use the thumbnail registry to get the details of the thumbnail
ThumbnailRegistry registry = thumbnailService.getThumbnailRegistry();
ThumbnailDefinition thumbnailDefinition = registry.getThumbnailDefinition(rendition.getId());
if (thumbnailDefinition == null)
{
throw new NotFoundException(rendition.getId() + " is not registered.");
}
ContentData contentData = getContentData(sourceNodeRef, true);
// Check if anything is currently available to generate thumbnails for the specified mimeType
if (!registry.isThumbnailDefinitionAvailable(contentData.getContentUrl(), contentData.getMimetype(), contentData.getSize(), sourceNodeRef,
thumbnailDefinition))
{
throw new InvalidArgumentException("Unable to create thumbnail '" + thumbnailDefinition.getName() + "' for " +
contentData.getMimetype() + " as no transformer is currently available.");
}
Action action = ThumbnailHelper.createCreateThumbnailAction(thumbnailDefinition, serviceRegistry);
// Queue async creation of thumbnail
actionService.executeAction(action, sourceNodeRef, true, true);
}
@Override
public BinaryResource getContent(String nodeId, String renditionId, Parameters parameters)
{
final NodeRef sourceNodeRef = validateSourceNode(nodeId);
final NodeRef renditionNodeRef = getRenditionByName(sourceNodeRef, renditionId, parameters);
// By default set attachment header (with rendition Id) unless attachment=false
boolean attach = true;
String attachment = parameters.getParameter("attachment");
if (attachment != null)
{
attach = Boolean.valueOf(attachment);
}
final String attachFileName = (attach ? renditionId : null);
if (renditionNodeRef == null)
{
boolean isPlaceholder = Boolean.valueOf(parameters.getParameter("placeholder"));
if (!isPlaceholder)
{
throw new NotFoundException("Thumbnail was not found for [" + renditionId + ']');
}
String sourceNodeMimeType = getMimeType(sourceNodeRef);
// resource based on the content's mimeType and rendition id
String phPath = scriptThumbnailService.getMimeAwarePlaceHolderResourcePath(renditionId, sourceNodeMimeType);
if (phPath == null)
{
// 404 since no thumbnail was found
throw new NotFoundException("Thumbnail was not found and no placeholder resource available for [" + renditionId + ']');
}
else
{
if (LOGGER.isDebugEnabled())
{
LOGGER.debug("Retrieving content from resource path [" + phPath + ']');
}
// get extension of resource
String ext = "";
int extIndex = phPath.lastIndexOf('.');
if (extIndex != -1)
{
ext = phPath.substring(extIndex);
}
try
{
final String resourcePath = "classpath:" + phPath;
InputStream inputStream = resourceLoader.getResource(resourcePath).getInputStream();
// create temporary file
File file = TempFileProvider.createTempFile(inputStream, "RenditionsApi-", ext);
return new FileBinaryResource(file, attachFileName);
}
catch (Exception ex)
{
if (LOGGER.isErrorEnabled())
{
LOGGER.error("Couldn't load the placeholder." + ex.getMessage());
}
new ApiException("Couldn't load the placeholder.");
}
}
}
Map nodeProps = nodeService.getProperties(renditionNodeRef);
ContentData contentData = (ContentData) nodeProps.get(ContentModel.PROP_CONTENT);
Date modified = (Date) nodeProps.get(ContentModel.PROP_MODIFIED);
org.alfresco.rest.framework.resource.content.ContentInfo contentInfo = null;
if (contentData != null)
{
contentInfo = new ContentInfoImpl(contentData.getMimetype(), contentData.getEncoding(), contentData.getSize(), contentData.getLocale());
}
// add cache settings
CacheDirective cacheDirective = new CacheDirective.Builder()
.setNeverCache(false)
.setMustRevalidate(false)
.setLastModified(modified)
.setETag(modified != null ? Long.toString(modified.getTime()) : null)
.setMaxAge(Long.valueOf(31536000))// one year (in seconds)
.build();
return new NodeBinaryResource(renditionNodeRef, ContentModel.PROP_CONTENT, contentInfo, attachFileName, cacheDirective);
}
protected NodeRef getRenditionByName(NodeRef nodeRef, String renditionId, Parameters parameters)
{
if (StringUtils.isEmpty(renditionId))
{
throw new InvalidArgumentException("renditionId can't be null or empty.");
}
// Thumbnails have a cm: prefix.
QName renditionQName = QName.resolveToQName(namespaceService, renditionId);
ChildAssociationRef nodeRefRendition = renditionService.getRenditionByName(nodeRef, renditionQName);
if (nodeRefRendition == null)
{
return null;
}
return nodeRefRendition.getChildRef();
}
protected Rendition toApiRendition(NodeRef renditionNodeRef)
{
Rendition apiRendition = new Rendition();
String renditionName = (String) nodeService.getProperty(renditionNodeRef, ContentModel.PROP_NAME);
apiRendition.setId(renditionName);
ContentData contentData = getContentData(renditionNodeRef, false);
ContentInfo contentInfo = null;
if (contentData != null)
{
contentInfo = new ContentInfo(contentData.getMimetype(),
getMimeTypeDisplayName(contentData.getMimetype()),
contentData.getSize(),
contentData.getEncoding());
}
apiRendition.setContent(contentInfo);
apiRendition.setStatus(RenditionStatus.CREATED);
return apiRendition;
}
protected Rendition toApiRendition(ThumbnailDefinition thumbnailDefinition)
{
ContentInfo contentInfo = new ContentInfo(thumbnailDefinition.getMimetype(),
getMimeTypeDisplayName(thumbnailDefinition.getMimetype()), null, null);
Rendition apiRendition = new Rendition();
apiRendition.setId(thumbnailDefinition.getName());
apiRendition.setContent(contentInfo);
apiRendition.setStatus(RenditionStatus.NOT_CREATED);
return apiRendition;
}
protected NodeRef validateSourceNode(String nodeId)
{
final NodeRef nodeRef = nodes.validateNode(nodeId);
if (!nodes.isSubClass(nodeRef, ContentModel.PROP_CONTENT, false))
{
throw new InvalidArgumentException("Node id '" + nodeId + "' does not represent a file.");
}
return nodeRef;
}
private String getMimeTypeDisplayName(String mimeType)
{
return mimetypeService.getDisplaysByMimetype().get(mimeType);
}
private ContentData getContentData(NodeRef nodeRef, boolean validate)
{
ContentData contentData = (ContentData) nodeService.getProperty(nodeRef, ContentModel.PROP_CONTENT);
if (validate && !ContentData.hasContent(contentData))
{
throw new InvalidArgumentException("Node id '" + nodeRef.getId() + "' has no content.");
}
return contentData;
}
private String getMimeType(NodeRef nodeRef)
{
ContentData contentData = getContentData(nodeRef, true);
return contentData.getMimetype();
}
}