/*
 * Copyright (C) 2005-2011 Alfresco Software Limited.
 *
 * This file is part of Alfresco
 *
 * 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 .
 */
package org.alfresco.repo.publishing;
import static org.alfresco.repo.publishing.PublishingModel.ASSOC_LAST_PUBLISHING_EVENT;
import static org.alfresco.repo.publishing.PublishingModel.ASSOC_PUBLISHING_EVENT;
import static org.alfresco.repo.publishing.PublishingModel.NAMESPACE;
import static org.alfresco.repo.publishing.PublishingModel.PROP_PUBLISHING_EVENT_CHANNEL;
import static org.alfresco.repo.publishing.PublishingModel.PROP_PUBLISHING_EVENT_COMMENT;
import static org.alfresco.repo.publishing.PublishingModel.PROP_PUBLISHING_EVENT_NODES_TO_PUBLISH;
import static org.alfresco.repo.publishing.PublishingModel.PROP_PUBLISHING_EVENT_NODES_TO_UNPUBLISH;
import static org.alfresco.repo.publishing.PublishingModel.PROP_PUBLISHING_EVENT_PAYLOAD;
import static org.alfresco.repo.publishing.PublishingModel.PROP_PUBLISHING_EVENT_STATUS;
import static org.alfresco.repo.publishing.PublishingModel.PROP_PUBLISHING_EVENT_TIME;
import static org.alfresco.repo.publishing.PublishingModel.PROP_PUBLISHING_EVENT_TIME_ZONE;
import static org.alfresco.repo.publishing.PublishingModel.PROP_PUBLISHING_EVENT_WORKFLOW_ID;
import static org.alfresco.repo.publishing.PublishingModel.PROP_STATUS_UPDATE_CHANNEL_NAMES;
import static org.alfresco.repo.publishing.PublishingModel.PROP_STATUS_UPDATE_MESSAGE;
import static org.alfresco.repo.publishing.PublishingModel.PROP_STATUS_UPDATE_NODE_REF;
import static org.alfresco.repo.publishing.PublishingModel.PROP_WF_PUBLISHING_EVENT;
import static org.alfresco.repo.publishing.PublishingModel.PROP_WF_SCHEDULED_PUBLISH_DATE;
import static org.alfresco.repo.publishing.PublishingModel.TYPE_PUBLISHING_EVENT;
import static org.alfresco.util.collections.CollectionUtils.filter;
import static org.alfresco.util.collections.CollectionUtils.isEmpty;
import static org.alfresco.util.collections.CollectionUtils.toListOfStrings;
import static org.alfresco.util.collections.CollectionUtils.transform;
import static org.alfresco.util.collections.CollectionUtils.transformFlat;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.node.NodeUtils;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.permissions.AccessDeniedException;
import org.alfresco.repo.transfer.TransferContext;
import org.alfresco.repo.transfer.manifest.TransferManifestNodeFactory;
import org.alfresco.repo.transfer.manifest.TransferManifestNormalNode;
import org.alfresco.repo.version.VersionModel;
import org.alfresco.repo.workflow.WorkflowModel;
import org.alfresco.service.cmr.publishing.NodeSnapshot;
import org.alfresco.service.cmr.publishing.PublishingDetails;
import org.alfresco.service.cmr.publishing.PublishingEvent;
import org.alfresco.service.cmr.publishing.PublishingEventFilter;
import org.alfresco.service.cmr.publishing.PublishingPackage;
import org.alfresco.service.cmr.publishing.PublishingPackageEntry;
import org.alfresco.service.cmr.publishing.Status;
import org.alfresco.service.cmr.publishing.StatusUpdate;
import org.alfresco.service.cmr.repository.AssociationRef;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.security.AccessStatus;
import org.alfresco.service.cmr.security.PermissionService;
import org.alfresco.service.cmr.transfer.TransferDefinition;
import org.alfresco.service.cmr.version.VersionService;
import org.alfresco.service.cmr.version.VersionType;
import org.alfresco.service.cmr.workflow.WorkflowDefinition;
import org.alfresco.service.cmr.workflow.WorkflowPath;
import org.alfresco.service.cmr.workflow.WorkflowService;
import org.alfresco.service.cmr.workflow.WorkflowTask;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.namespace.RegexQNamePattern;
import org.alfresco.util.GUID;
import org.alfresco.util.collections.Filter;
import org.alfresco.util.collections.Function;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
 * @author Brian
 * @author Nick Smith
 * @since 4.0
 */
public class PublishingEventHelper
{
    private static final Log log = LogFactory.getLog(PublishingEventHelper.class);
    public static final String WORKFLOW_DEFINITION_NAME = "publishWebContent";
    private NodeService nodeService;
    private ContentService contentService;
    private VersionService versionService;
    private WorkflowService workflowService;
    private NodeSnapshotSerializer serializer;
    private PermissionService permissionService;
    private TransferManifestNodeFactory transferManifestNodeFactory;
    private List excludedAspects = new ArrayList();
    private String workflowEngineId;
    private TransferDefinition excludedAspectsTransferDefinition;
    
    /**
     * @param nodeService
     *            the nodeService to set
     */
    public void setNodeService(NodeService nodeService)
    {
        this.nodeService = nodeService;
    }
    /**
     * @param contentService
     *            the contentService to set
     */
    public void setContentService(ContentService contentService)
    {
        this.contentService = contentService;
    }
    /**
     * @param transferManifestNodeFactory the transferManifestNodeFactory to set
     */
    public void setTransferManifestNodeFactory(TransferManifestNodeFactory transferManifestNodeFactory)
    {
        this.transferManifestNodeFactory = transferManifestNodeFactory;
    }
    
    /**
     * @param versionService the versionService to set
     */
    public void setVersionService(VersionService versionService)
    {
        this.versionService = versionService;
    }
    
    /**
     * @param workflowService the workflowService to set
     */
    public void setWorkflowService(WorkflowService workflowService)
    {
        this.workflowService = workflowService;
    }
    
    /**
     * @param workflowEngineId the workflowEngineId to set
     */
    public void setWorkflowEngineId(String workflowEngineId)
    {
        this.workflowEngineId = workflowEngineId;
    }
    
    /**
     * @param serializer the serializer to set
     */
    public void setSerializer(NodeSnapshotSerializer serializer)
    {
        this.serializer = serializer;
    }
    /**
     * @param permissionService the permissionService to set
     */
    public void setPermissionService(PermissionService permissionService)
    {
        this.permissionService = permissionService;
    }
    
    public PublishingEvent getPublishingEvent(NodeRef eventNode) throws AlfrescoRuntimeException
    {
        if (eventNode == null)
        {
            return null;
        }
        
        Map props = nodeService.getProperties(eventNode);
        String statusStr = (String) props.get(PROP_PUBLISHING_EVENT_STATUS);
        Status status = Status.valueOf(statusStr);
        String channel = (String) props.get(PROP_PUBLISHING_EVENT_CHANNEL);
        Date createdTime = (Date) props.get(ContentModel.PROP_CREATED);
        String creator = (String) props.get(ContentModel.PROP_CREATOR);
        Date modifiedTime = (Date) props.get(ContentModel.PROP_MODIFIED);
        String modifier = (String) props.get(ContentModel.PROP_MODIFIER);
        String comment = (String) props.get(PROP_PUBLISHING_EVENT_COMMENT);
        Calendar scheduledTime = getScheduledTime(props);
        PublishingPackage publishingPackage = getPublishingPackage(eventNode, channel);
        StatusUpdate statusUpdate = buildStatusUpdate(props);
        return new PublishingEventImpl(eventNode.toString(),
                status, channel,
                publishingPackage, createdTime,
                creator,modifiedTime, modifier,
                scheduledTime, comment, statusUpdate);
    }
    @SuppressWarnings("unchecked")
    private StatusUpdate buildStatusUpdate(Map props)
    {
        String message = (String) props.get(PROP_STATUS_UPDATE_MESSAGE);
        Collection channelNames = (Collection) props.get(PROP_STATUS_UPDATE_CHANNEL_NAMES);
        if (channelNames == null || channelNames.isEmpty())
        {
            return null;
        }
        String nodeId = (String) props.get(PROP_STATUS_UPDATE_NODE_REF);
        NodeRef nodeToLinkTo = nodeId==null ? null : new NodeRef(nodeId);
        return new StatusUpdateImpl(message, nodeToLinkTo, channelNames);
    }
    public List getPublishingEvents(List eventNodes)
    {
        return transform(eventNodes, new Function()
                {
                    public PublishingEvent apply(NodeRef eventNode)
                    {
                        return getPublishingEvent(eventNode);
                    }
                });
    }
    
    public NodeRef createNode(NodeRef queueNode, PublishingDetails details) throws Exception
    {
        checkChannelAccess(details.getPublishChannelId());
        Set statusChannelIds = details.getStatusUpdateChannels();
        if (isEmpty(statusChannelIds) == false)
        for (String statusChannelId : statusChannelIds)
        {
            checkChannelAccess(statusChannelId);
        }
        String name = GUID.generate();
        Map props = buildPublishingEventProperties(details, name);
        ChildAssociationRef newAssoc = nodeService.createNode(queueNode, 
                ASSOC_PUBLISHING_EVENT,
                QName.createQName(NAMESPACE, name),
                TYPE_PUBLISHING_EVENT, props);
        NodeRef eventNode = newAssoc.getChildRef();
        serializePublishNodes(eventNode, details);
        return eventNode;
    }
    private void checkChannelAccess(String channelId)
    {
        NodeRef channelNode = new NodeRef(channelId);
        AccessStatus accessStatus = permissionService.hasPermission(channelNode, PermissionService.ADD_CHILDREN);
        if (AccessStatus.ALLOWED != accessStatus)
        {
            throw new AccessDeniedException("You do not have access to channel: " + channelId);
        }
    }
    private Map buildPublishingEventProperties(PublishingDetails details, String name)
    {
        Calendar schedule = details.getSchedule();
        if (schedule == null)
        {
            schedule = Calendar.getInstance();
        }
        Map props = new HashMap();
        props.put(ContentModel.PROP_NAME, name);
        props.put(PROP_PUBLISHING_EVENT_STATUS, Status.IN_PROGRESS.name());
        props.put(PROP_PUBLISHING_EVENT_TIME, schedule.getTime());
        props.put(PublishingModel.PROP_PUBLISHING_EVENT_TIME_ZONE, schedule.getTimeZone().getID());
        props.put(PublishingModel.PROP_PUBLISHING_EVENT_CHANNEL, details.getPublishChannelId());
        props.put(PublishingModel.PROP_PUBLISHING_EVENT_STATUS, PublishingModel.PROPVAL_PUBLISHING_EVENT_STATUS_SCHEDULED);
        String comment = details.getComment();
        if (comment != null)
        {
            props.put(PROP_PUBLISHING_EVENT_COMMENT, comment);
        }
        Collection publshStrings = mapNodesToStrings(details.getNodesToPublish());
        props.put(PROP_PUBLISHING_EVENT_NODES_TO_PUBLISH, (Serializable) publshStrings);
        Collection unpublshStrings = mapNodesToStrings(details.getNodesToUnpublish());
        props.put(PROP_PUBLISHING_EVENT_NODES_TO_UNPUBLISH, (Serializable) unpublshStrings);
        String message = details.getStatusMessage();
        Set statusChannels = details.getStatusUpdateChannels();
        if (message != null && isEmpty(statusChannels) == false)
        {
            props.put(PROP_STATUS_UPDATE_MESSAGE, message);
            NodeRef statusNode = details.getNodeToLinkTo();
            if (statusNode != null)
            {
                props.put(PROP_STATUS_UPDATE_NODE_REF, statusNode.toString());
            }
            props.put(PROP_STATUS_UPDATE_CHANNEL_NAMES, (Serializable) statusChannels);
        }
        return props;
    }
    private List mapNodesToStrings(Collection nodes)
    {
        return toListOfStrings(nodes);
    }
    public List findPublishingEventNodes(final NodeRef queue, PublishingEventFilter filter)
    {
        List eventNodes;
        Set publishedNodes = filter.getPublishedNodes();
        if (isEmpty(publishedNodes) == false)
        {
            eventNodes= getEventNodesForPublishedNodes(queue, publishedNodes);
        }
        else
        {
            eventNodes = getAllPublishingEventNodes(queue);
        }
        Set ids = filter.getIds();
        if (isEmpty(ids) == false)
        {
            eventNodes = filterEventNodesById(eventNodes, ids);
        }
        return eventNodes;
    }
    private List filterEventNodesById(Collection eventNodes, final Collection ids)
    {
        return filter(eventNodes, new Filter()
        {
            public Boolean apply(NodeRef node)
            {
                return ids.contains(node.toString());
            }
        });
    }
    private List getAllPublishingEventNodes(final NodeRef queue)
    {
        List assocs =
            nodeService.getChildAssocs(queue, ASSOC_PUBLISHING_EVENT, RegexQNamePattern.MATCH_ALL);
        return transform(assocs, NodeUtils.toChildRef());
    }
    /**
     * Returns a {@link List} of the {@link NodeRef}s representing PublishingEvents that were scheduled to publish at least one of the specified publishedNodes. 
     * @param queue NodeRef
     * @param publishedNodes NodeRef..
     * @return List
     */
    public List getEventNodesForPublishedNodes(final NodeRef queue, NodeRef... publishedNodes)
    {
        return getEventNodesForPublishedNodes(queue, Arrays.asList(publishedNodes));
    }
    /**
     * Returns a {@link List} of the {@link NodeRef}s representing PublishingEvents that were scheduled to publish at least one of the specified publishedNodes. 
     * @param queue NodeRef
     * @param publishedNodes Collection
     * @return List
     */
    public List getEventNodesForPublishedNodes(final NodeRef queue, Collection publishedNodes)
    {
        return getEventNodesForNodeProperty(queue, PROP_PUBLISHING_EVENT_NODES_TO_PUBLISH, publishedNodes);
    }
    
    /**
     * Returns a {@link List} of the {@link NodeRef}s representing PublishingEvents that were scheduled to unpublish at least one of the specified unpublishedNodes. 
     * @param queue NodeRef
     * @param unpublishedNodes Collection
     * @return List
     */
    public List getEventNodesForUnpublishedNodes(final NodeRef queue, Collection unpublishedNodes)
    {
        return getEventNodesForNodeProperty(queue, PROP_PUBLISHING_EVENT_NODES_TO_PUBLISH, unpublishedNodes);
    }
    
    /**
     * Returns a {@link List} of the {@link NodeRef}s representing PublishingEvents that were scheduled to publish the specified publishedNode. 
     * @param queue NodeRef
     * @param publishedNode NodeRef
     * @return List
     */
    public List getEventNodesForPublishedNode(final NodeRef queue, NodeRef publishedNode)
    {
        Function> transformer = eventNodeForNodePropertyFinder(queue, PROP_PUBLISHING_EVENT_NODES_TO_PUBLISH);
        return transformer.apply(publishedNode);
    }
    
    /**
     * Returns a {@link List} of the {@link NodeRef}s representing PublishingEvents that were scheduled to unpublish the specified unpublishedNode. 
     * @param queue NodeRef
     * @param unpublishedNode NodeRef
     * @return List
     */
    public List getEventNodesForUnpublishedNode(final NodeRef queue, NodeRef unpublishedNode)
    {
        Function> transformer = eventNodeForNodePropertyFinder(queue, PROP_PUBLISHING_EVENT_NODES_TO_UNPUBLISH);
        return transformer.apply(unpublishedNode);
    }
    private List getEventNodesForNodeProperty(final NodeRef queue, final QName propertyKey, Collection publishedNodes)
    {
        Function> transformer = eventNodeForNodePropertyFinder(queue, propertyKey);
        return transformFlat(publishedNodes, transformer);
    }
    private Function> eventNodeForNodePropertyFinder(final NodeRef queue,
            final QName propertyKey)
    {
        return new Function>()
        {
            public List apply(NodeRef publishedNode)
            {
                String nodeString = publishedNode.toString();
                List assocs = 
                    nodeService.getChildAssocsByPropertyValue(queue, propertyKey, nodeString);
                return transform(assocs, NodeUtils.toChildRef());
            }
        };
    }
    public List findPublishingEvents(NodeRef queue, PublishingEventFilter filter)
    {
        List eventNodes = findPublishingEventNodes(queue, filter);
        return getPublishingEvents(eventNodes);
    }
    
    public PublishingEvent getPublishingEvent(String id)
    {
        NodeRef eventNode = getPublishingEventNode(id);
        return getPublishingEvent(eventNode);
    }
    
    public NodeRef getPublishingEventNode(String id)
    {
        if (id != null && NodeRef.isNodeRef(id))
        {
            NodeRef eventNode = new NodeRef(id);
            if (nodeService.exists(eventNode) && TYPE_PUBLISHING_EVENT.equals(nodeService.getType(eventNode)))
            {
                return eventNode;
            }
        }
        return null;
    }
    
    public String startPublishingWorkflow(NodeRef eventNode, Calendar scheduledTime)
    {
        //Set parameters
        Map parameters = new HashMap();
        parameters.put(PROP_WF_PUBLISHING_EVENT, eventNode);
        parameters.put(WorkflowModel.ASSOC_PACKAGE, workflowService.createPackage(null));
        parameters.put(PROP_WF_SCHEDULED_PUBLISH_DATE, scheduledTime);
        
        //Start workflow
        WorkflowPath path = workflowService.startWorkflow(getPublshingWorkflowDefinitionId(), parameters);
        String instanceId = path.getInstance().getId();
        
        //Set the Workflow Id on the event node.
        nodeService.setProperty(eventNode, PROP_PUBLISHING_EVENT_WORKFLOW_ID, instanceId);
        
        //End the start task.
        //TODO Replace with endStartTask() call after merge to HEAD.
        WorkflowTask startTask = workflowService.getStartTask(instanceId);
        workflowService.endTask(startTask.getId(), null);
        return instanceId;
    }
    
    private String getPublshingWorkflowDefinitionId()
    {
        String definitionName = workflowEngineId + "$" + WORKFLOW_DEFINITION_NAME;
        WorkflowDefinition definition = workflowService.getDefinitionByName(definitionName);
        if (definition == null)
        {
            String msg = "The Web publishing workflow definition does not exist! Definition name: " + definitionName;
            throw new AlfrescoRuntimeException(msg);
        }
        return definition.getId();
    }
    public Calendar getScheduledTime(NodeRef eventNode)
    {
        if (eventNode == null)
        {
            return null;
        }
        return getScheduledTime(nodeService.getProperties(eventNode));
    }
    public Calendar getScheduledTime(Map eventProperties)
    {
        Date time = (Date) eventProperties.get(PROP_PUBLISHING_EVENT_TIME);
        String timezone= (String) eventProperties.get(PROP_PUBLISHING_EVENT_TIME_ZONE);
        Calendar scheduledTime = Calendar.getInstance();
        scheduledTime.setTime(time);
        scheduledTime.setTimeZone(TimeZone.getTimeZone(timezone));
        return scheduledTime;
    }
    
    private void serializePublishNodes(NodeRef eventNode, PublishingDetails details) throws Exception
    {
        try
        {
            NodeRef channelNode = new NodeRef(details.getPublishChannelId());
            List snapshots = createPublishSnapshots(details.getNodesToPublish());
            snapshots.addAll(createUnpublishSnapshots(details.getNodesToUnpublish(), channelNode));
            ContentWriter contentWriter = contentService.getWriter(eventNode,
                    PROP_PUBLISHING_EVENT_PAYLOAD, true);
            contentWriter.setEncoding("UTF-8");
            OutputStream os = contentWriter.getContentOutputStream();
            serializer.serialize(snapshots, os);
            os.flush();
            os.close();
        }
        catch (Exception ex)
        {
            log.warn("Failed to serialize publishing package", ex);
            throw ex;
        }
    }
    
    private List createUnpublishSnapshots(Set nodes, final NodeRef channelNode)
    {
        return transform(nodes, new Function()
        {
            public NodeSnapshot apply(NodeRef node)
            {
                return createUnpublishSnapshot(node, channelNode);
            }
        });
    }
    private PublishingPackage getPublishingPackage(NodeRef eventNode, String channelId) throws AlfrescoRuntimeException
    {
        Map entries = getPublishingPackageEntries(eventNode);
        return new PublishingPackageImpl(entries);
    }
    private List createPublishSnapshots(final Collection nodes)
    {
        return AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork>()
        {
            public List doWork() throws Exception
            {
                return transform(nodes, new Function()
                        {
                            public NodeSnapshot apply(NodeRef node)
                            {
                                return createPublishSnapshot(node);
                            }
                        });
            }
        }, AuthenticationUtil.getSystemUserName());
    }
    
    private NodeSnapshotTransferImpl createPublishSnapshot(NodeRef node)
    {
        if (!nodeService.hasAspect(node, ContentModel.ASPECT_VERSIONABLE))
        {
            Map props = new HashMap(1, 1.0f);
            props.put(ContentModel.PROP_AUTO_VERSION, true);
            props.put(ContentModel.PROP_AUTO_VERSION_PROPS, false);
            versionService.ensureVersioningEnabled(node, props);
        }
        versionService.createVersion(node, null);
        TransferManifestNormalNode payload = (TransferManifestNormalNode) transferManifestNodeFactory.createTransferManifestNode(node, excludedAspectsTransferDefinition, new TransferContext());
        NodeSnapshotTransferImpl snapshot = new NodeSnapshotTransferImpl(payload);
        return snapshot;
    }
    @SuppressWarnings("unchecked")
    private Map getPublishingPackageEntries(NodeRef eventNode)
    {
        List idsToUnpublish = (List) nodeService.getProperty(eventNode, PROP_PUBLISHING_EVENT_NODES_TO_UNPUBLISH);
        List nodesToUnpublish = NodeUtils.toNodeRefs(idsToUnpublish);
        ContentReader contentReader = contentService.getReader(eventNode, PROP_PUBLISHING_EVENT_PAYLOAD);
        InputStream input = contentReader.getContentInputStream();
        try
        {
            List snapshots = serializer.deserialize(input);
            Map entries = new HashMap(snapshots.size());
            for (NodeSnapshot snapshot : snapshots)
            {
                NodeRef node = snapshot.getNodeRef();
                boolean isPublish = false == nodesToUnpublish.contains(node);
                PublishingPackageEntryImpl entry = new PublishingPackageEntryImpl(isPublish, node, snapshot);
                entries.put(node, entry);
            }
            return entries;
        }
        catch (Exception ex)
        {
            String msg ="Failed to deserialize publishing package for PublishingEvent: " +eventNode;
            throw new AlfrescoRuntimeException(msg, ex);
        }
    }
    private NodeSnapshot createUnpublishSnapshot(NodeRef source, NodeRef channelNode)
    {
        NodeRef lastEvent = getLastPublishEvent(source, channelNode);
        if (lastEvent == null)
        {
            String msg = "Cannot create unpublish snapshot as last publishing event does not exist! Source node: "+ source + " channelId: "+channelNode;
            throw new AlfrescoRuntimeException(msg);
        }
        Map entries = getPublishingPackageEntries(lastEvent);
        PublishingPackageEntry entry = entries.get(source);
        return entry.getSnapshot();
    }
    
    public NodeRef getLastPublishEvent(NodeRef source, NodeRef channelNode)
    {
        NodeRef publishedNode = ChannelHelper.mapSourceToEnvironment(source, channelNode, nodeService);
        if (publishedNode == null)
        {
            return null;
        }
        List assocs = nodeService.getTargetAssocs(publishedNode, ASSOC_LAST_PUBLISHING_EVENT);
        return NodeUtils.getSingleAssocNode(assocs, true);
    }
    
    public void cancelEvent(String id)
    {
        NodeRef eventNode = getPublishingEventNode(id);
        if (eventNode != null)
        {
            Map eventProps = nodeService.getProperties(eventNode);
            String status = (String)eventProps.get(PublishingModel.PROP_PUBLISHING_EVENT_STATUS);
            //If this event has not started to be processed yet then we can stop the associated workflow and
            //delete the event...
            if (PublishingModel.PROPVAL_PUBLISHING_EVENT_STATUS_SCHEDULED.equals(status))
            {
                //Get hold of the process id
                String processId = (String)eventProps.get(PublishingModel.PROP_PUBLISHING_EVENT_WORKFLOW_ID);
                if (processId != null)
                {
                    workflowService.cancelWorkflow(processId);
                }
                nodeService.deleteNode(eventNode);
            }
            
            //Otherwise, if the current event is being processed now we just set its status to "CANCELLED REQUESTED"
            else if (PublishingModel.PROPVAL_PUBLISHING_EVENT_STATUS_IN_PROGRESS.equals(status))
            {
                nodeService.setProperty(eventNode, PublishingModel.PROP_PUBLISHING_EVENT_STATUS, PublishingModel.PROPVAL_PUBLISHING_EVENT_STATUS_CANCEL_REQUESTED);
            }
            
            //Otherwise this event has already been processed or has already been cancelled. Do nothing.
        }
    }
    
    public AssociationRef linkToLastEvent(NodeRef publishedNode, NodeRef eventNode)
    {
        List assocs = nodeService.getTargetAssocs(publishedNode, ASSOC_LAST_PUBLISHING_EVENT);
        if (isEmpty(assocs) == false)
        {
            // Remove old association.
            AssociationRef assoc = assocs.get(0);
            nodeService.removeAssociation(assoc.getSourceRef(), assoc.getTargetRef(), assoc.getTypeQName());
        }
        return nodeService.createAssociation(publishedNode, eventNode, ASSOC_LAST_PUBLISHING_EVENT);
    }
    public PublishingDetails createPublishingDetails()
    {
        return new PublishingDetailsImpl();
    }
    /**
     * Sets a list of excluded aspects, assumes the fully qualified name.  Replaces any exising excluded aspects.
     * @param excludedAspects Collection
     */
    public void setExcludedAspects(Collection excludedAspects)
    {
        this.excludedAspects.clear();
        
        for (String aspect : excludedAspects)
        {
            this.excludedAspects.add(QName.createQName(aspect));
        }
        this.excludedAspectsTransferDefinition = new TransferDefinition();
        excludedAspectsTransferDefinition.setExcludedAspects(this.excludedAspects);
    }   
}