com.day.cq.dam.core.process.XMPWritebackProcess Maven / Gradle / Ivy
/*************************************************************************
* ADOBE CONFIDENTIAL
* ___________________
*
* Copyright 2011 Adobe Systems Incorporated
* All Rights Reserved.
*
* NOTICE: All information contained herein is, and remains
* the property of Adobe Systems Incorporated and its suppliers,
* if any. The intellectual and technical concepts contained
* herein are proprietary to Adobe Systems Incorporated and its
* suppliers and are protected by trade secret or copyright law.
* Dissemination of this information or reproduction of this material
* is strictly forbidden unless prior written permission is obtained
* from Adobe Systems Incorporated.
**************************************************************************/
package com.day.cq.dam.core.process;
import static com.day.cq.commons.jcr.JcrConstants.JCR_CONTENT;
import static com.day.cq.commons.jcr.JcrConstants.JCR_LAST_MODIFIED_BY;
import static com.day.cq.dam.api.DamConstants.PN_VERSION_CREATOR;
import com.adobe.internal.pdftoolkit.core.util.Utility;
import org.apache.commons.codec.digest.DigestUtils;
import com.adobe.xmp.XMPException;
import com.adobe.xmp.core.XMPArray;
import com.adobe.xmp.core.XMPNode;
import com.adobe.xmp.core.XMPSimple;
import com.adobe.xmp.core.XMPStruct;
import com.adobe.xmp.core.parser.RDFXMLParser;
import com.adobe.xmp.core.parser.RDFXMLParserContext;
import com.day.cq.dam.api.handler.AssetHandler;
import com.day.cq.dam.api.metadata.ExtractedMetadata;
import com.day.cq.dam.commons.metadata.XmpFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.PushbackInputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferenceCardinality;
import org.apache.felix.scr.annotations.ReferencePolicy;
import org.apache.felix.scr.annotations.Service;
import org.apache.sling.api.resource.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.adobe.xmp.XMPConst;
import com.adobe.xmp.core.XMPMetadata;
import com.day.cq.dam.api.Asset;
import com.day.cq.dam.api.Rendition;
import com.day.cq.dam.api.handler.xmp.XMPHandler;
import com.day.cq.dam.api.handler.xmp.XMPWriteBackOptions;
import com.day.cq.dam.commons.process.AbstractAssetWorkflowProcess;
import com.day.cq.dam.core.impl.PrivateConstants;
import com.day.cq.dam.core.impl.handler.xmp.XMPWriteBackOptionsImpl;
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 static com.day.cq.dam.api.DamConstants.PN_SHA1;
/**
* XMPWritebackProcess
writes back meta data to the original binary
*
* This process is executed on any changes made under the metadata node
* {/content/dam(...)/metadata}.
*/
@Component(metatype = false)
@Service
@Properties({@org.apache.felix.scr.annotations.Property(name = "process.label", value = "XMP Writeback")})
public class XMPWritebackProcess extends AbstractAssetWorkflowProcess {
private static final String PS_AUX_ISO = "psAux:ISO";
private static final String EXIF_FLASH = "Flash";
private static final int MAGIC_SIZE = 1024;
private static final String EPS_MIMETYPE = "application/postscript";
/**
* PostScript start.
*/
private static final byte[] PS_START = "%!".getBytes();
/**
* Adobe marker.
*/
private static final byte[] PS_ADOBE = "PS-Adobe-".getBytes();
/**
* EPS type.
*/
private static final byte[] EPS_TYPE = "EPS".getBytes();
private static final Logger log = LoggerFactory.getLogger(XMPWritebackProcess.class);
public enum Arguments {
PROCESS_ARGS("PROCESS_ARGS"), CREATE_VERSION("createversion"), RENDITION("rendition");
private String argumentName;
Arguments(String argumentName) {
this.argumentName = argumentName;
}
public String getArgumentName() {
return this.argumentName;
}
public String getArgumentPrefix() {
return this.argumentName + ":";
}
}
@Reference(policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.OPTIONAL_UNARY)
protected XMPHandler xmpHandler;
@Reference
private XmpFilter xmpFilter;
public void execute(WorkItem workItem, WorkflowSession workflowSession,
MetaDataMap metaDataMap) throws WorkflowException {
if (null == xmpHandler) {
log.warn("XMP Writeback is not supported on this platform or required libraries are missing !!!");
workflowSession.terminateWorkflow(workItem.getWorkflow());
return;
}
String[] args = buildArguments(metaDataMap);
String payloadPath = null;
if (workItem.getWorkflowData().getPayloadType().equals(TYPE_JCR_PATH)) {
payloadPath = workItem.getWorkflowData().getPayload().toString();
}
log.info("payload path :" + payloadPath);
final Session session = workflowSession.getSession();
final Asset asset = getAssetFromPayload(workItem, session);
String userId = workItem.getWorkflowData().getMetaDataMap().get("userId", String.class);
if (null != asset && null != payloadPath) {
try {
final Node assetNode = asset.adaptTo(Node.class);
final Node content = assetNode.getNode(JCR_CONTENT);
// moved this piece of code here, so that newRendition property
// is removed even if the mimetype is not supported.
// this is to stop itself to get in to infinite loop because of
// update asset work flow
final Node payloadNode = session.getNode(payloadPath);
if (payloadNode.hasProperty(PrivateConstants.SYNC_FLAG)) {
log.debug("Skipping the {} execution.",
XMPWritebackProcess.class.getName());
payloadNode.getProperty(PrivateConstants.SYNC_FLAG).remove();
session.save();
workflowSession.terminateWorkflow(workItem.getWorkflow());
return;
}
if (content.hasProperty(PrivateConstants.SYNC_FLAG)) {
log.debug("Skipping the {} execution.",
XMPWritebackProcess.class.getName());
content.getProperty(PrivateConstants.SYNC_FLAG).remove();
session.save();
workflowSession.terminateWorkflow(workItem.getWorkflow());
} else {
String mime = asset.getMimeType();
boolean createVersion = true;
if (args.length > 0) {
createVersion = "true".equals(getValuesFromArgs("createversion", args).get(0)) ? true : false;
}
if (xmpHandler.isSupported(mime) && isWritebackSupported(asset)) {
XMPWriteBackOptions writeBackOptions = new XMPWriteBackOptionsImpl();
writeBackOptions.createVersion(createVersion);
List renditions = getValuesFromArgs("rendition", args);
Set XMPWriteBackRenditions = new HashSet();
for (String rendition : renditions) {
Rendition currentRendition = asset.getRendition(rendition);
if (null != currentRendition) {
XMPWriteBackRenditions.add(currentRendition);
}
}
writeBackOptions.setRenditions(XMPWriteBackRenditions);
writeXmp(asset, workItem, workflowSession, writeBackOptions);
Rendition orgRen = asset.getOriginal();
Node orgRendNode = orgRen.adaptTo(Node.class);
Node orgRendContNode = orgRendNode.getNode(JCR_CONTENT);
InputStream is = null;
try {
is = orgRen.getStream();
final String sha1 = DigestUtils.shaHex(is);
Node metaData = asset.adaptTo(Node.class).getNode(JCR_CONTENT).getNode("metadata");
metaData.setProperty(PN_SHA1, sha1);
}
finally{
IOUtils.closeQuietly(is);
}
orgRendContNode.setProperty(JCR_LAST_MODIFIED_BY, userId);
content.setProperty(JCR_LAST_MODIFIED_BY, userId);
} else {
log.info(
"XMP Writeback is not supported for type {} using implementation {} for the asset located at {}",
mime, xmpHandler.getClass().getName(), asset.getPath());
}
}
} catch (Throwable e) {
log.error(e.getMessage());
log.debug("Stack Trace", e);
try {
if (session.hasPendingChanges()) {
session.refresh(false);
}
} catch (RepositoryException re) {
log.error("Failed to refresh workflow session", re);
}
}
} else {
String wfPayload = workItem.getWorkflowData().getPayload().toString();
String message = "execute: cannot writeback xmp, asset [{" + wfPayload + "}] in payload doesn't exist for workflow [{" + workItem.getId() + "}].";
throw new WorkflowException(message);
}
}
/**
* This is to check conditions such as the version of the format is
* supported for write back or not.
*
* @param asset
* @return
*/
private boolean isWritebackSupported(Asset asset) {
String mimeType = asset.getMimeType();
if (EPS_MIMETYPE.equals(mimeType)) {
/*
* writeback only supported for Illustrator 9 EPS files onwards
* CQ-41659
*/
PushbackInputStream pin = null;
try {
pin = new PushbackInputStream(
asset.getOriginal().getStream(), MAGIC_SIZE);
byte[] data = new byte[MAGIC_SIZE];
int len;
if ((len = pin.read(data)) <= 0) {
// no content
return false;
}
pin.unread(data, 0, len);
if(! isIllustratorPDFFile(data, 0, len)) {
// its an EPS file , look for the EPS version marker
double adobeMarker = getAdobeMarkerFromEPS(data, len);
if (adobeMarker > 3.0) {
return true;
} else {
return false;
}
}
else{
return true; // writeback is supported for AI PDF files
}
} catch (Exception e) {
log.warn("error while reading AI/EPS file: [{}]: ", e);
}
finally {
if (pin!=null){
try {
pin.close();
} catch (IOException e) {
log.warn("error while closing AI/EPS file input stream: [{}]: ", e);
}
}
}
}
return true;
}
private double getAdobeMarkerFromEPS(byte[] data, int len) {
double adobeMarker = 0.0;
int off = locate(PS_START, data, 0, len);
if (off == -1) {
return adobeMarker;
}
off = locate(PS_ADOBE, data, off, len);
if (off == -1) {
return adobeMarker;
}
int epsTypeLocation = locate(EPS_TYPE, data, off, len);
if (epsTypeLocation == -1) {
return adobeMarker;
}
String adobeMarkerString = new String(data, off,
(epsTypeLocation - off));
try {
adobeMarker = new Double(adobeMarkerString.replace(new String(
EPS_TYPE), "").trim());
} catch (NumberFormatException ne) {
log.warn("Exception occured while reading PS_ADOBE marker from eps file: "
+ ne.getMessage());
}
return adobeMarker;
}
/**
* Return whether the data
is from an Adobe Illustrator (AI) PDF file.
* NOTE: Illustrator created PDF files have "application/postscript" as their MIME
*
* @param data data buffer
* @param off offset
* @param len number of valid bytes
* @return true
if its an illustrator file; false
otherwise
*/
private boolean isIllustratorPDFFile(byte[] data, int off, int len){
// Is this stream a PDF data stream?
// See if you can find the PDF marker in the first 1024 bytes.
// see PDF 1.5, Appendix H, note 13
//check if it is an AI PDF file with application/postscript as mimetype
byte[] pdfMarker = {'%', 'P', 'D', 'F', '-'};
int size = 1024;
if (size > len) size = len;
byte[] header = new byte[size];
System.arraycopy(data, off, header, 0, size);
long result = Utility.KMPFindFirst(pdfMarker, Utility.ComputeKMPNextArray(pdfMarker), header);
return (result >= 0);
}
private int locate(byte[] pattern, byte[] data, int off, int len) {
int i = 0;
while (i < pattern.length && off < len) {
if (pattern[i] == data[off]) {
i++;
} else {
i = 0;
}
off++;
}
return i == pattern.length ? off : -1;
}
/**
* todo use JCR to XMPMeta reader if available Its a very simple writeback
* to support flat meta data properties
*/
private void writeXmp(Asset asset, WorkItem workItem,
WorkflowSession workflowSession, XMPWriteBackOptions writeBackOptions) {
final Session session = workflowSession.getSession();
try {
final Node assetNode = asset.adaptTo(Node.class);
final Node content = assetNode.getNode(JCR_CONTENT);
XMPMetadata metadata = asset.adaptTo(Resource.class).adaptTo(
com.adobe.granite.asset.api.Asset.class).getAssetMetadata().getXMP();
//workaround: remove aux:ISO from being written back
//TODO: follow up with XMP team so that this can also be ignored as other exif values are
if (metadata.get(XMPConst.NS_EXIF_AUX, PS_AUX_ISO) != null) {
metadata.setSimple(XMPConst.NS_EXIF_AUX, PS_AUX_ISO, "");
}
/*
* workaround: Remove exif:Flash metadata .It causes XmpWritebackProcess to fail
* TODO: follow up with XMP team so that this can also be ignored as other exif values are.
*/
if (metadata.get(XMPConst.NS_EXIF, EXIF_FLASH) != null) {
metadata.remove(XMPConst.NS_EXIF, EXIF_FLASH);
}
if (xmpFilter != null && xmpFilter.isActive()) {
/* With an active ingestion filter, there might have been metadata in the original document that we never
* persisted in the repository as jcr properties. If we now write back the jcr properties, they will replace
* any existing XMP in the original and filtered props will be lost. So, before we do that, we re-read the
* complete XMP from the original, 'sieve' out the properties we ignored and add them before the overwrite.
*
* CAVEAT: if we *partially* ignore properties (as in array elements), the read-back will replace the jcr
* property with the full original values. Therefore, editing such properties and writing back will not work!
*/
final AssetHandler handler = getAssetHandler(asset.getMimeType());
if (null != handler) {
final ExtractedMetadata origdata = handler.extractMetadata(asset);
if ( null != origdata.getXmp()) {
RDFXMLParser parser = new RDFXMLParser();
RDFXMLParserContext pctx = new RDFXMLParserContext();
XMPMetadata originalMeta = parser.parse(xmpFilter.sieve(origdata.getXmp()), pctx);
if (log.isDebugEnabled()) {
log.debug("metadata ignored by ingestion: " + originalMeta.dump());
}
mergeMeta(metadata, originalMeta);
} // else there's nothing to merge
}
}
// get user id
String userId = workItem.getWorkflowData().getMetaDataMap().get("userId", String.class);
//set version creator, this is used by AssetManager.createRevision
content.setProperty(PN_VERSION_CREATOR, userId);
xmpHandler.writeXmpMetadata(asset, metadata, writeBackOptions);
} catch (Throwable e) {
log.warn("XMP Writeback is not supported on this platform or required libraries are missing !!!");
log.debug("Stack Trace", e);
try {
if (session.hasPendingChanges()) {
session.refresh(false);
}
} catch (RepositoryException re) {
log.error("Failed to refresh workflow session", re);
}
}
}
synchronized void bindXmpHandler(final XMPHandler handler) {
xmpHandler = handler;
log.debug("binding xmp handler");
}
synchronized void unbindXmpHandler(final XMPHandler handler) {
xmpHandler = null;
log.debug("un-binding xmp handler");
}
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 createVersion = metaData.get(Arguments.CREATE_VERSION.name(), String.class);
if (StringUtils.isNotBlank(createVersion)) {
StringBuilder builder = new StringBuilder();
builder.append(Arguments.CREATE_VERSION.getArgumentPrefix()).append(createVersion);
arguments.add(builder.toString());
}
String[] renditions = metaData.get(Arguments.RENDITION.name(), String[].class);
if (renditions != null) {
for (String rendition : renditions) {
StringBuilder builder = new StringBuilder();
builder.append(Arguments.RENDITION.getArgumentPrefix()).append(rendition);
arguments.add(builder.toString());
}
}
return arguments.toArray(new String[arguments.size()]);
}
}
private void mergeMeta(XMPMetadata to, XMPMetadata from) throws XMPException {
for (XMPNode n : from) {
/* Adding to struct or array? */
XMPNode dest = to.remove(n.getNamespace(), n.getName());
if (log.isDebugEnabled()) {
log.debug("merge Node " + n);
if (dest != null) {
log.debug("property {" + dest.getNamespace() + "}" + dest.getName() + " is overwritten");
}
}
if (n instanceof XMPSimple) {
to.copy((XMPSimple) n);
} else if (n instanceof XMPStruct) {
to.copy((XMPStruct) n);
} else if (n instanceof XMPArray) {
to.copy((XMPArray) n);
}
}
}
}