com.day.cq.dam.core.process.CommandLineProcess Maven / Gradle / Ivy
/*
* Copyright 1997-2008 Day Management AG
* Barfuesserplatz 6, 4001 Basel, Switzerland
* All Rights Reserved.
*
* This software is the confidential and proprietary information of
* Day Management AG, ("Confidential Information"). You shall not
* disclose such Confidential Information and shall use it only in
* accordance with the terms of the license agreement you entered into
* with Day.
*/
package com.day.cq.dam.core.process;
import aQute.bnd.annotation.ProviderType;
import com.day.cq.dam.api.Asset;
import com.day.cq.dam.api.Rendition;
import com.day.cq.dam.api.handler.AssetHandler;
import com.day.cq.dam.api.renditions.RenditionMaker;
import com.day.cq.dam.api.renditions.RenditionTemplate;
import com.day.cq.dam.api.thumbnail.ThumbnailConfig;
import com.day.cq.dam.commons.process.AbstractAssetWorkflowProcess;
import com.day.cq.dam.commons.util.AssetUpdate;
import com.day.cq.dam.commons.util.AssetUpdateMonitor;
import com.day.cq.workflow.WorkflowException;
import com.day.cq.workflow.WorkflowSession;
import com.day.cq.workflow.exec.WorkItem;
import com.day.cq.workflow.metadata.MetaDataMap;
import org.apache.commons.exec.CommandLine;
import org.apache.commons.exec.DefaultExecutor;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
import org.apache.jackrabbit.util.Text;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.jcr.RepositoryException;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Workflow process that calls a command-line program and uses each output file
* that it produces as an additional rendition. Optionally creates thumbnails
* based on those renditions.
* The dimensions given in the thumbnail specifications are the maximal size the
* result must have. Aspect ratio is maintained for image resizing. As a
* precondition, the payload of this Workflow process has to be an
* {@link com.day.cq.dam.api.Asset Asset} or part of an Asset.
*
* Example with the following workflow step arguments:
*
* mime:application/postscript,
* tn:140:100,tn:48:48,
* cmd:/bin/convert ${directory}/${filename} ${directory}/${basename}.jpg
*
*
* The process will call /bin/convert, pass it the full path of the asset being
* processed (temporarily dumped to disk), and create thumbnails of size 140x100
* and 48x48 based on the output created by /bin/convert.
* This will only happen for assets having the
* application/postscript
mime-type, others are ignored.
*
* Arguments:
*
*
*
* Name
* Prefix
* Description
* Required
* Multiple
* Example
*
*
*
* Command
* cmd:
* Command as executed on the consle.
* The command can contain varibales which are replaced before execution. The
* following variables are available:
* filename: name of the file as exported to disk.
* file: absolute path of the file exported.
* directory: absolute path of the directory the command is run and the
* asset is exported to.
* basename: the assets name on the disk without possible file-extension
* extension: the assets file extension.
*
* required
* multiple
* cmd:/bin/convert ${directory}/${filename} ${directory}/${basename}.jpg
*
*
* Mimetype Filter
* mime:
* Mimetype the Asset must have. If the Asset is of a diffrent type, this
* process will not be executed.
* required
* multiple
* mime:application/postscript
*
*
* Thumbnail Specification
* tn:
* Dimensions of the Thumbnails to be generated from the Asset.
* width:height[:false]
* width:Number the maximal width in Pixel the Thumbnail must not exceed
* height:Number the maximal height in Pixel the Thumbnail must not
* exceed. center:Boolean, optional flag to indicate that the resulting
* Thumbnail must not be centered, true is default.
*
* multiple
* tn:140:100
*
*
*
* @see AbstractAssetWorkflowProcess
*/
@Component(metatype = false)
@Service
@Property(name = "process.label", value = "Command Line")
@ProviderType
public class CommandLineProcess extends AbstractAssetWorkflowProcess {
private final Logger log = LoggerFactory.getLogger(getClass());
/**
* The available arguments to this process implementation.
*/
public enum Arguments {
PROCESS_ARGS("PROCESS_ARGS"),
/** Argument for the mime types to use */
MIME_TYPES("mime"),
/** Argument for thumb nail sizes */
THUMBNAILS("tn"),
/** Argument for command-line to execute */
COMMANDS("cmd"),
/** Argument to generate web renditions **/
GENERATE_WEB_RENDITION("genWebRendition"),
/** Argument to delete rendition generated by command prompt **/
DELETE_COMMAND_RENDITION("deleteCommandRendition");
private String argumentName;
Arguments(String argumentName) {
this.argumentName = argumentName;
}
public String getArgumentName() {
return this.argumentName;
}
public String getArgumentPrefix() {
return this.argumentName + ":";
}
}
@Reference
protected RenditionMaker renditionMaker;
@Reference
private AssetUpdateMonitor monitor;
private CreateWebEnabledImageProcess webEnabledImageCreator = new CreateWebEnabledImageProcess();
public void execute(final WorkItem workItem, WorkflowSession wfsession, final MetaDataMap args) throws WorkflowException {
final AssetUpdate update = monitor.startUpdate(workItem, getResourceResolver(wfsession.getSession()), this);
final String[] arguments = buildArguments(args);
update.checkAndRun(new AssetUpdate.AssetCheck() {
@Override
public boolean isAcceptable(Asset asset) throws WorkflowException {
// Process only specific mime types, based on arguments
final List mimeTypes = new LinkedList();
final String assetMimeType = asset.getMimeType();
for (String str : arguments) {
if (str.startsWith(Arguments.MIME_TYPES.getArgumentPrefix())) {
final String mt = str.substring(Arguments.MIME_TYPES.getArgumentPrefix().length()).trim();
log.debug("execute: accepted mime type [{}] for asset [{}].", mt, asset.getPath());
mimeTypes.add(mt);
}
}
if (!mimeTypes.contains(assetMimeType)) {
log.info("execute: mime type [{}] of asset [{}] is not in list of accepted mime types [" + mimeTypes
+ "], ignoring.", assetMimeType, asset.getPath());
return false;
}
return true;
}
@Override
public boolean isNullAcceptable() throws WorkflowException {
String wfPayload = workItem.getWorkflowData().getPayload().toString();
String message = "execute: cannot extract metadata, create references and apply processing profile, asset [{"
+ wfPayload + "}] in payload doesn't exist for workflow [{"
+ workItem.getId() + "}].";
throw new WorkflowException(message);
}
}, new AssetUpdate.Runner() {
@Override
public void run(Asset asset, AssetUpdate update) throws WorkflowException, Exception {
File tmpDir = null;
InputStream is = null;
OutputStream os = null;
try {
// creating temp directory
tmpDir = File.createTempFile("cqdam", null);
tmpDir.delete();
tmpDir.mkdir();
// make sure that tumbnails are not processed again, otherwise you
// will end in a endless recursion
final Rendition original = asset.getOriginal();
// streaming file to temp directory
final File tmpFile = new File(tmpDir, Text.escape( asset.getName()));
OutputStream fos = new FileOutputStream(tmpFile);
InputStream in = null;
try {
in = original.getStream();
IOUtils.copy(in, fos);
} finally {
IOUtils.closeQuietly(is);
IOUtils.closeQuietly(fos);
}
// building command line
CommandLine commandLine;
String lastLine = "";
Map parameters = new HashMap();
parameters.put("filename", tmpFile.getName());
parameters.put("file", tmpFile.getAbsolutePath());
parameters.put("directory", tmpDir.getAbsolutePath());
parameters.put("basename", tmpFile.getName().replaceFirst("\\..*$", ""));
parameters.put("extension", tmpFile.getName().replaceFirst("^.*\\.", ""));
try {
for (String argument : arguments) {
if (argument.startsWith(Arguments.COMMANDS.getArgumentPrefix())) {
// Execute command line
final String cmd = argument.substring(Arguments.COMMANDS.getArgumentPrefix().length())
.trim();
commandLine = CommandLine.parse(cmd, parameters);
lastLine = commandLine.toString();
DefaultExecutor exec = new DefaultExecutor();
exec.setWorkingDirectory(tmpDir);
log.info("execute: executing command line [{}] for asset [{}].", lastLine, asset.getPath());
// No need to check the exit value, we get
// an Exception from the executor if it's not 0
exec.execute(commandLine);
}
}
} catch (Exception e) {
log.error("execute: failed to execute command [{}] for asset [" + asset.getPath() + "]: ",
lastLine, e);
}
// go over all result files.
for (File result : tmpDir.listFiles(new FileFilter() {
public boolean accept(File pathname) {
// ignore the original file
return !pathname.equals(tmpFile);
}
})) {
// Stream output to rendition node
final Rendition rendition = asset.addRendition(result.getName(), new FileInputStream(result),
recheck(result.getName()));
// Extract thumbnail dimensions from args
final Set thumbnailConfigs = new HashSet();
for (final String str : arguments) {
int indexOf = str.indexOf(Arguments.THUMBNAILS.getArgumentPrefix());
if (indexOf > -1) {
final ThumbnailConfig config = CreateThumbnailProcess.parseThumbnailArguments(str.substring(indexOf
+ Arguments.THUMBNAILS.getArgumentPrefix().length()));
if (null != config) {
thumbnailConfigs.add(config);
log.debug("execute: thumbnail dimensions [{}] for asset [{}].", str, asset.getPath());
} else {
log.error("execute: cannot add invalid thumbnail config [{}] for asset [{}].", str,
asset.getPath());
}
}
}
List templates = createRenditionTemplates(rendition,
thumbnailConfigs.toArray(new ThumbnailConfig[] {}));
log.debug("thumbnail template created at [{}] with [{}] thumbnails for [" + asset.getPath() + "].",
rendition.getPath(), templates.size());
Boolean createWebRend = args.get(Arguments.GENERATE_WEB_RENDITION.name(), Boolean.class);
// create web-enabled rendition if flag is enabled
if (createWebRend != null && createWebRend) {
CreateWebEnabledImageProcess.Config config = webEnabledImageCreator.parseConfig(args);
RenditionTemplate webRendTemp = renditionMaker.createWebRenditionTemplate(rendition,
config.width, config.height, config.quality, config.mimeType, config.mimeTypesToKeep);
templates.add(webRendTemp);
log.debug("Web rendition template created at [{}] with [{}] thumbnails for [" + asset.getPath()
+ "].", rendition.getPath());
}
// generate all thumbnail & web enabled renditions
renditionMaker.generateRenditions(asset, templates.toArray(new RenditionTemplate[] {}));
// if flag is enabled remove rendition generated by
// command.
Boolean delCommRend = args.get(Arguments.DELETE_COMMAND_RENDITION.name(), Boolean.class);
if (delCommRend != null && delCommRend) {
asset.removeRendition(rendition.getName());
}
}
} finally {
// cleaning up temp directory
IOUtils.closeQuietly(is);
IOUtils.closeQuietly(os);
if (tmpDir != null) {
try {
FileUtils.deleteDirectory(tmpDir);
} catch (IOException e) {
throw new WorkflowException(e);
}
}
}
}
});
}
protected void createThumbnails(final Asset asset, final Rendition rendition,
final Collection configs) throws Exception {
final String mimeType = rendition.getMimeType();
final AssetHandler handler = getAssetHandler(mimeType);
if (handler == null) {
throw new IOException("No AssetHandler found for mimetype " + mimeType);
}
log.debug("createThumbnails: generating thumbnails for rendition [{}] with mime type [{}]...", asset.getPath(),
mimeType);
handler.createThumbnails(asset, rendition, configs);
}
protected String recheck(String fileName) throws RepositoryException {
if (mimeTypeService.getMimeType(fileName.toLowerCase()) != null) {
return mimeTypeService.getMimeType(fileName.toLowerCase());
}
return "application/octet-stream";
}
public String[] buildArguments(MetaDataMap metaData) {
// the 'old' way, ensures backward compatibility
String processArgs = metaData.get(Arguments.PROCESS_ARGS.name(), String.class);
if (processArgs != null && !processArgs.equals("")) {
return processArgs.split(",");
} else {
List arguments = new ArrayList();
String[] commands = metaData.get(Arguments.COMMANDS.name(), String[].class);
if (commands != null) {
for (String command : commands) {
StringBuilder builder = new StringBuilder();
builder.append(Arguments.COMMANDS.getArgumentPrefix()).append(command);
arguments.add(builder.toString());
}
}
String[] mimetypes = metaData.get(Arguments.MIME_TYPES.name(), String[].class);
if (mimetypes != null) {
for (String mimetype : mimetypes) {
StringBuilder builder = new StringBuilder();
builder.append(Arguments.MIME_TYPES.getArgumentPrefix()).append(mimetype);
arguments.add(builder.toString());
}
}
String[] thumbnails = metaData.get(Arguments.THUMBNAILS.name(), String[].class);
if (thumbnails != null) {
for (String thumbnail : thumbnails) {
StringBuilder builder = new StringBuilder();
builder.append(Arguments.THUMBNAILS.getArgumentPrefix()).append(thumbnail);
arguments.add(builder.toString());
}
}
return arguments.toArray(new String[arguments.size()]);
}
}
private List createRenditionTemplates(Rendition rendition, ThumbnailConfig[] thumbnails) {
List templates = new ArrayList(thumbnails.length);
for (int i = 0; i < thumbnails.length; i++) {
ThumbnailConfig thumb = thumbnails[i];
templates.add(renditionMaker.createThumbnailTemplate(rendition, thumb.getWidth(), thumb.getHeight(),
thumb.doCenter()));
}
return templates;
}
}