All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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); } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy