/* * #%L * ACS AEM Commons Bundle * %% * Copyright (C) 2015 Adobe * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * #L% */ package com.adobe.acs.commons.workflow.synthetic.impl; import com.adobe.acs.commons.workflow.synthetic.SyntheticWorkflowModel; import com.adobe.acs.commons.workflow.synthetic.SyntheticWorkflowRunner; import com.adobe.acs.commons.workflow.synthetic.impl.cq.SyntheticWorkItem; import com.adobe.acs.commons.workflow.synthetic.impl.cq.SyntheticWorkflow; import com.adobe.acs.commons.workflow.synthetic.impl.cq.SyntheticWorkflowSession; import com.adobe.acs.commons.workflow.synthetic.impl.cq.exceptions.SyntheticCompleteWorkflowException; import com.adobe.acs.commons.workflow.synthetic.impl.cq.exceptions.SyntheticRestartWorkflowException; import com.adobe.acs.commons.workflow.synthetic.impl.cq.exceptions.SyntheticTerminateWorkflowException; import com.day.cq.workflow.WorkflowException; import com.day.cq.workflow.WorkflowService; import com.day.cq.workflow.WorkflowSession; import com.day.cq.workflow.exec.WorkflowProcess; import org.apache.felix.scr.annotations.Activate; import org.apache.felix.scr.annotations.Component; import org.apache.felix.scr.annotations.Deactivate; 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.References; import org.apache.felix.scr.annotations.Service; import org.apache.sling.api.resource.LoginException; import org.apache.sling.api.resource.ResourceResolver; import org.apache.sling.api.resource.ResourceResolverFactory; import org.apache.sling.commons.osgi.PropertiesUtil; import org.apache.sling.jcr.resource.JcrResourceConstants; import org.osgi.framework.BundleContext; import org.osgi.framework.ServiceRegistration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.jcr.RepositoryException; import javax.jcr.Session; import java.util.Date; import java.util.Dictionary; import java.util.HashMap; import java.util.Hashtable; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * ACS AEM Commons - Synthetic Workflow Runner * Facilitates the execution of synthetic workflow. */ @Component(immediate = true) @References({ @Reference( referenceInterface = WorkflowProcess.class, policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE, bind = "bindCqWorkflowProcesses", unbind = "unbindCqWorkflowProcesses" ), @Reference( referenceInterface = com.adobe.granite.workflow.exec.WorkflowProcess.class, policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE, bind = "bindGraniteWorkflowProcesses", unbind = "unbindGraniteWorkflowProcesses" ) }) // Explicitly register to the SyntheticWorkflowRunner interface (as this extends WorkflowService, which we do not want to register a service against) @Service(value = SyntheticWorkflowRunner.class) public class SyntheticWorkflowRunnerImpl implements SyntheticWorkflowRunner { private static final Logger log = LoggerFactory.getLogger(SyntheticWorkflowRunnerImpl.class); private static final String UNSUPPORTED_OPERATION_MESSAGE = "Operation not supported by Synthetic Workflow"; private static final String WORKFLOW_PROCESS_LABEL = "process.label"; private static final int MAX_RESTART_COUNT = 3; private Map<String, SyntheticWorkflowProcess> workflowProcessesByLabel = new ConcurrentHashMap<String, SyntheticWorkflowProcess>(); private Map<String, SyntheticWorkflowProcess> workflowProcessesByProcessName = new ConcurrentHashMap<String, SyntheticWorkflowProcess>(); @Reference private WorkflowService aemWorkflowService; @Reference private ResourceResolverFactory resourceResolverFactory; private ServiceRegistration accessorReg; /** * {@inheritDoc} */ @Override public final void execute(final ResourceResolver resourceResolver, final String payloadPath, final String[] workflowProcessLabels) throws WorkflowException { this.execute(resourceResolver, payloadPath, workflowProcessLabels, null, false, false); } /** * {@inheritDoc} */ @Override public final void execute(final ResourceResolver resourceResolver, final String payloadPath, final String[] workflowProcessLabels, Map<String, Map<String, Object>> processArgs, final boolean autoSaveAfterEachWorkflowProcess, final boolean autoSaveAtEnd) throws WorkflowException { this.execute(resourceResolver, payloadPath, WorkflowProcessIdType.PROCESS_LABEL, workflowProcessLabels, processArgs, autoSaveAfterEachWorkflowProcess, autoSaveAtEnd); } /** * {@inheritDoc} */ @Override public final void execute(final ResourceResolver resourceResolver, final String payloadPath, final WorkflowProcessIdType workflowProcessIdType, final String[] workflowProcessIds, Map<String, Map<String, Object>> processArgs, final boolean autoSaveAfterEachWorkflowProcess, final boolean autoSaveAtEnd) throws WorkflowException { final long start = System.currentTimeMillis(); if (processArgs == null) { processArgs = new HashMap<String, Map<String, Object>>(); } int count = 0; do { count++; try { run(resourceResolver, payloadPath, workflowProcessIdType, workflowProcessIds, processArgs, autoSaveAfterEachWorkflowProcess, autoSaveAtEnd); if (log.isInfoEnabled()) { long duration = System.currentTimeMillis() - start; log.info("Synthetic workflow execution of payload [ {} ] completed in [ {} ] ms", payloadPath, duration); } return; } catch (SyntheticRestartWorkflowException ex) { if (count < MAX_RESTART_COUNT) { log.info("Restarting CQ synthetic workflow for [ {} ]", payloadPath); } else { log.warn("Synthetic CQ workflow execution of payload [ {} ] reached max restart rate of [ {} ]", payloadPath, count); } } } while (count < MAX_RESTART_COUNT); } private void run(final ResourceResolver resourceResolver, final String payloadPath, final WorkflowProcessIdType workflowProcessIdType, final String[] workflowProcessIds, final Map<String, Map<String, Object>> metaDataMaps, final boolean autoSaveAfterEachWorkflowProcess, final boolean autoSaveAtEnd) throws WorkflowException { final Session session = resourceResolver.adaptTo(Session.class); // Create the WorkflowData obj; This will persist through all WF Process Steps // This must be remained defined once to it can be shared by reference across CQ and Granite SyntheticWorkflow isntances final SyntheticWorkflowData workflowData = new SyntheticWorkflowData("JCR_PATH", payloadPath); // Create the Workflow obj; This will persist through all WF Process Steps // The Workflow MetadataMap will leverage the WorkflowData's MetadataMap as these two maps should be in sync final SyntheticWorkflow cqWorkflow = new SyntheticWorkflow("Synthetic Workflow ( " + payloadPath + " )", workflowData); final com.adobe.acs.commons.workflow.synthetic.impl.granite.SyntheticWorkflow graniteWorkflow = new com.adobe.acs.commons.workflow.synthetic.impl.granite.SyntheticWorkflow("Synthetic Workflow ( " + payloadPath + " )", workflowData); boolean terminated = false; for (final String workflowProcessId : workflowProcessIds) { SyntheticWorkflowProcess workflowProcess; if (WorkflowProcessIdType.PROCESS_LABEL.equals(workflowProcessIdType)) { workflowProcess = this.workflowProcessesByLabel.get(workflowProcessId); } else { workflowProcess = this.workflowProcessesByProcessName.get(workflowProcessId); } if (workflowProcess != null) { final long start = System.currentTimeMillis(); try { final SyntheticMetaDataMap workflowProcessMetaDataMap = new SyntheticMetaDataMap(metaDataMaps.get(workflowProcessId)); if (SyntheticWorkflowProcess.Type.GRANITE.equals(workflowProcess.getWorkflowType())) { runGraniteWorkflowProcess(session, graniteWorkflow, workflowProcessMetaDataMap, workflowProcess); } else if (SyntheticWorkflowProcess.Type.CQ.equals(workflowProcess.getWorkflowType())) { runCqWorkflowProcess(session, cqWorkflow, workflowProcessMetaDataMap, workflowProcess); } else { log.warn("Workflow process step is of an unknown type [ {} ]. Skipping.", workflowProcess.getWorkflowType()); } } catch (SyntheticTerminateWorkflowException ex) { // Terminate entire Workflow execution for this payload terminated = true; log.info("Synthetic CQ workflow execution stopped via terminate for [ {} ]", payloadPath); break; } catch (com.adobe.acs.commons.workflow.synthetic.impl.granite.exceptions.SyntheticTerminateWorkflowException ex) { // Terminate entire Workflow execution for this payload terminated = true; log.info("Synthetic Granite workflow execution stopped via terminate for [ {} ]", payloadPath); break; } catch (SyntheticRestartWorkflowException ex) { // Handle CQ Restart Workflow; catch/throw for clarity in whats happening throw ex; } catch (com.adobe.acs.commons.workflow.synthetic.impl.granite.exceptions.SyntheticRestartWorkflowException ex) { // Handle Granite Restart Exceptions by transforming them into CQ Worlflow Restart Exceptions which the rest of this API leverages throw new SyntheticRestartWorkflowException(ex.getMessage()); } catch (WorkflowException ex) { // Handle CQ Workflow Exception; catch/throw for clarity in whats happening throw ex; } catch (com.adobe.granite.workflow.WorkflowException ex) { // Handle Granite Workflow Exceptions by transforming them into CQ Workflow Exceptions which the rest of this API leverages throw new WorkflowException(ex); } finally { try { if (!terminated && autoSaveAfterEachWorkflowProcess && session.hasPendingChanges()) { session.save(); } log.debug("Executed synthetic workflow process [ {} ] on [ {} ] in [ " + String.valueOf(System.currentTimeMillis() - start) + " ] ms", workflowProcessId, payloadPath); } catch (RepositoryException e) { log.error("Could not save at end of synthetic workflow process execution" + " [ {} ] for payload path [ {} ]", workflowProcessId, payloadPath); log.error("Synthetic workflow process save failed.", e); throw new WorkflowException(e); } } } else { log.error("Synthetic workflow runner retrieved a null Workflow Process for process.label [ {} ]", workflowProcessId); } } // end for loop try { if (autoSaveAtEnd && session.hasPendingChanges()) { session.save(); } } catch (RepositoryException e) { log.error("Could not complete save at end of synthetic workflow execution process" + " [ {} ]", payloadPath, e); throw new WorkflowException(e); } } private void runCqWorkflowProcess(Session session, SyntheticWorkflow workflow, SyntheticMetaDataMap workflowProcessMetaDataMap, SyntheticWorkflowProcess workflowProcess) throws WorkflowException { final WorkflowSession workflowSession = this.getCqWorkflowSession(session); // Each Workflow Process Step gets its own workItem whose life starts and ends w the WF Process final SyntheticWorkItem workItem = new SyntheticWorkItem(workflow.getWorkflowData()); workItem.setWorkflow(workflow); log.trace("Executing CQ synthetic workflow process [ {} ] on [ {} ]", workflowProcess.getProcessId(), workflow.getWorkflowData().getPayload()); // Execute the Workflow Process try { workflowProcess.getCqWorkflowProcess().execute(workItem, workflowSession, workflowProcessMetaDataMap); workItem.setTimeEnded(new Date()); } catch (SyntheticCompleteWorkflowException ex) { // Workitem force-completed via a call to workflowSession.complete(..) workItem.setTimeEnded(new Date()); log.trace(ex.getMessage()); } catch (SyntheticTerminateWorkflowException ex) { workItem.setTimeEnded(new Date()); log.trace(ex.getMessage()); throw ex; } } private void runGraniteWorkflowProcess(Session session, com.adobe.acs.commons.workflow.synthetic.impl.granite.SyntheticWorkflow workflow, SyntheticMetaDataMap workflowProcessMetaDataMap, SyntheticWorkflowProcess workflowProcess) throws com.adobe.granite.workflow.WorkflowException { final com.adobe.acs.commons.workflow.synthetic.impl.granite.SyntheticWorkflowSession workflowSession = this.getGraniteWorkflowSession(session); // Each Workflow Process Step gets its own workItem whose life starts and ends w the WF Process final com.adobe.acs.commons.workflow.synthetic.impl.granite.SyntheticWorkItem workItem = new com.adobe.acs.commons.workflow.synthetic.impl.granite.SyntheticWorkItem(workflow.getWorkflowData()); workItem.setWorkflow(workflow); log.trace("Executing Granite synthetic workflow process [ {} ] on [ {} ]", workflowProcess.getProcessId(), workflow.getWorkflowData().getPayload()); // Execute the Workflow Process try { workflowProcess.getGraniteWorkflowProcess().execute(workItem, workflowSession, workflowProcessMetaDataMap); workItem.setTimeEnded(new Date()); } catch (com.adobe.acs.commons.workflow.synthetic.impl.granite.exceptions.SyntheticCompleteWorkflowException ex) { // Workitem force-completed via a call to workflowSession.complete(..) workItem.setTimeEnded(new Date()); log.trace(ex.getMessage()); } catch (com.adobe.acs.commons.workflow.synthetic.impl.granite.exceptions.SyntheticTerminateWorkflowException ex) { workItem.setTimeEnded(new Date()); log.trace(ex.getMessage()); throw ex; } } @Override public final void execute(final ResourceResolver resourceResolver, final String payloadPath, final SyntheticWorkflowModel syntheticWorkflowModel, final boolean autoSaveAfterEachWorkflowProcess, final boolean autoSaveAtEnd) throws WorkflowException { final String[] processNames = syntheticWorkflowModel.getWorkflowProcessNames(); final Map<String, Map<String, Object>> processConfigs = syntheticWorkflowModel.getSyntheticWorkflowModelData(); execute(resourceResolver, payloadPath, WorkflowProcessIdType.PROCESS_NAME, processNames, processConfigs, autoSaveAfterEachWorkflowProcess, autoSaveAtEnd); } @Override public final SyntheticWorkflowModel getSyntheticWorkflowModel(final ResourceResolver resourceResolver, final String workflowModelId, final boolean ignoreIncompatibleTypes) throws WorkflowException { final WorkflowSession workflowSession = aemWorkflowService.getWorkflowSession(resourceResolver.adaptTo(Session.class)); return new SyntheticWorkflowModelImpl(workflowSession, workflowModelId, ignoreIncompatibleTypes); } /** * Unsupported operation. * * @throws WorkflowException */ @Override public final void start() throws WorkflowException { throw new UnsupportedOperationException(UNSUPPORTED_OPERATION_MESSAGE); } /** * Unsupported operation. */ @Override public final void stop() { throw new UnsupportedOperationException(UNSUPPORTED_OPERATION_MESSAGE); } /** * Deprecated. Please use getCqWorkflowSession(..) * Creates a Synthetic Workflow Session from a JCR Session. * * @param session the JCR Session to create the Synthetic Workflow Session from * @return the Synthetic Workflow Session */ @Deprecated public final WorkflowSession getWorkflowSession(final Session session) { return getCqWorkflowSession(session); } /** * Creates a CQ Synthetic Workflow Session from a JCR Session. * * @param session the JCR Session to create the Synthetic Workflow Session from * @return the CQ Synthetic Workflow Session */ public final WorkflowSession getCqWorkflowSession(final Session session) { return new SyntheticWorkflowSession(this, session); } /** * Creates a Granite Synthetic Workflow Session from a JCR Session. * * @param session the JCR Session to create the Synthetic Workflow Session from * @return the Granite Synthetic Workflow Session */ public final com.adobe.acs.commons.workflow.synthetic.impl.granite.SyntheticWorkflowSession getGraniteWorkflowSession(final Session session) { return new com.adobe.acs.commons.workflow.synthetic.impl.granite.SyntheticWorkflowSession(this, session); } /** * Getter for the AEM Workflow Service; This is only available at the Impl level and not part of the public * SyntheticWorkflowRunner interface. * The use of this service should be well understand as to prevent potential overhead of the the non-synthetic * aspects of WF. * * @return the AEM Workflow Service */ public final WorkflowService getAEMWorkflowService() { return this.aemWorkflowService; } public final ResourceResolver getResourceResolver(Session session) throws LoginException { Map<String, Object> authInfo = new HashMap<String, Object>(); authInfo.put(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, session); return resourceResolverFactory.getResourceResolver(authInfo); } @Deprecated @Override public final Dictionary<String, Object> getConfig() { return new Hashtable<String, Object>(); } @Activate protected final void activate(BundleContext bundleContext) { this.accessorReg = bundleContext.registerService(SyntheticWorkflowRunnerAccessor.class.getName(), new SyntheticWorkflowRunnerAccessor() { @Override public SyntheticWorkflowRunner getSyntheticWorkflowRunner() { return SyntheticWorkflowRunnerImpl.this; } }, new Hashtable()); } @Deactivate protected final void deactivate(final Map<String, Object> config) { log.trace("Deactivating Synthetic Workflow Runner"); this.workflowProcessesByLabel = new ConcurrentHashMap<String, SyntheticWorkflowProcess>(); this.workflowProcessesByProcessName = new ConcurrentHashMap<String, SyntheticWorkflowProcess>(); if (accessorReg != null) { accessorReg.unregister(); accessorReg = null; } } protected final void bindCqWorkflowProcesses(final WorkflowProcess service, final Map<Object, Object> props) { bindSyntheticWorkflowProcesses(new SyntheticWorkflowProcess(service), props); } protected final void unbindCqWorkflowProcesses(final WorkflowProcess service, final Map<Object, Object> props) { unbindSyntheticWorkflowProcesses(new SyntheticWorkflowProcess(service), props); } protected final void bindGraniteWorkflowProcesses(final com.adobe.granite.workflow.exec.WorkflowProcess service, final Map<Object, Object> props) { bindSyntheticWorkflowProcesses(new SyntheticWorkflowProcess(service), props); } protected final void unbindGraniteWorkflowProcesses(final com.adobe.granite.workflow.exec.WorkflowProcess service, final Map<Object, Object> props) { unbindSyntheticWorkflowProcesses(new SyntheticWorkflowProcess(service), props); } protected final void bindSyntheticWorkflowProcesses(final SyntheticWorkflowProcess process, final Map<Object, Object> props) { if (process != null) { // Workflow Process Labels final String label = PropertiesUtil.toString(props.get(WORKFLOW_PROCESS_LABEL), null); if (label != null) { this.workflowProcessesByLabel.put(label, process); log.trace("Synthetic {} Workflow Runner added Workflow Process by Label [ {} ]", process.getWorkflowType(), label); } // Workflow Process Name String processName = (String) process.getProcessId(); if (processName != null) { this.workflowProcessesByProcessName.put(processName, process); log.trace("Synthetic {} Workflow Runner added Workflow Process by Process Name [ {} ]", process.getWorkflowType(), processName); } else { log.trace("Process name is null for [ {} ]", label); } } } protected final void unbindSyntheticWorkflowProcesses(final SyntheticWorkflowProcess process, final Map<Object, Object> props) { if (process != null) { // Workflow Process Labels final String label = PropertiesUtil.toString(props.get(WORKFLOW_PROCESS_LABEL), null); if (label != null) { this.workflowProcessesByLabel.remove(label); log.trace("Synthetic {} Workflow Runner removed Workflow Process by Label [ {} ]", process.getWorkflowType(), label); } // Workflow Process Name String processName = (String) process.getProcessId(); if (processName != null) { this.workflowProcessesByProcessName.remove(processName); log.trace("Synthetic {} Workflow Runner removed Workflow Process by Process Name [ {} ]", process.getWorkflowType(), processName); } else { log.trace("Process name is null for [ {} ]", label); } } } }