/*
 * Copyright (C) 2005-2009 Alfresco Software Limited.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * This program 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 General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 * As a special exception to the terms and conditions of version 2.0 of 
 * the GPL, you may redistribute this Program in connection with Free/Libre 
 * and Open Source Software ("FLOSS") applications as described in Alfresco's 
 * FLOSS exception.  You should have recieved a copy of the text describing 
 * the FLOSS exception, and it is also available here: 
 * http://www.alfresco.com/legal/licensing"
 */
package org.alfresco.wcm.sandbox;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.mbeans.VirtServerRegistry;
import org.alfresco.model.WCMAppModel;
import org.alfresco.model.WCMWorkflowModel;
import org.alfresco.repo.avm.AVMNodeConverter;
import org.alfresco.repo.avm.actions.AVMRevertStoreAction;
import org.alfresco.repo.avm.actions.AVMUndoSandboxListAction;
import org.alfresco.repo.domain.PropertyValue;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.permissions.AccessDeniedException;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
import org.alfresco.repo.transaction.RetryingTransactionHelper;
import org.alfresco.repo.transaction.TransactionListenerAdapter;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.repo.workflow.WorkflowModel;
import org.alfresco.service.cmr.action.Action;
import org.alfresco.service.cmr.action.ActionService;
import org.alfresco.service.cmr.avm.AVMNodeDescriptor;
import org.alfresco.service.cmr.avm.AVMService;
import org.alfresco.service.cmr.avm.VersionDescriptor;
import org.alfresco.service.cmr.avmsync.AVMDifference;
import org.alfresco.service.cmr.avmsync.AVMSyncService;
import org.alfresco.service.cmr.dictionary.DataTypeDefinition;
import org.alfresco.service.cmr.repository.NodeRef;
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.cmr.workflow.WorkflowTaskState;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.NameMatcher;
import org.alfresco.util.Pair;
import org.alfresco.util.ParameterCheck;
import org.alfresco.util.VirtServerUtils;
import org.alfresco.wcm.asset.AssetInfo;
import org.alfresco.wcm.asset.AssetInfoImpl;
import org.alfresco.wcm.asset.AssetService;
import org.alfresco.wcm.util.WCMUtil;
import org.alfresco.wcm.util.WCMWorkflowUtil;
import org.alfresco.wcm.webproject.WebProjectInfo;
import org.alfresco.wcm.webproject.WebProjectService;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
 * Sandbox Service fundamental API.
 * 
 * This service API is designed to support the public facing Sandbox APIs. 
 * 
 * @author janv
 */
public class SandboxServiceImpl implements SandboxService
{
    /** Logger */
    private static Log logger = LogFactory.getLog(SandboxServiceImpl.class);
    
    private static final String WORKFLOW_SUBMITDIRECT = "jbpm$wcmwf:submitdirect";
    
    private WebProjectService wpService;
    private SandboxFactory sandboxFactory;
    private AVMService avmService;
    private AVMSyncService avmSyncService;
    private NameMatcher nameMatcher;
    private VirtServerRegistry virtServerRegistry;
    private ActionService actionService;
    private WorkflowService workflowService;
    private AssetService assetService;
    private TransactionService transactionService;
    
    
    public void setWebProjectService(WebProjectService wpService)
    {
        this.wpService = wpService;
    }
    public void setSandboxFactory(SandboxFactory sandboxFactory)
    {
        this.sandboxFactory = sandboxFactory;
    }
    
    public void setAvmService(AVMService avmService)
    {
        this.avmService = avmService;
    }
    
    public void setAvmSyncService(AVMSyncService avmSyncService)
    {
        this.avmSyncService = avmSyncService;
    }
    
    public void setNameMatcher(NameMatcher nameMatcher)
    {
       this.nameMatcher = nameMatcher;
    }
    
    public void setVirtServerRegistry(VirtServerRegistry virtServerRegistry)
    {
        this.virtServerRegistry = virtServerRegistry;
    }
    
    public void setActionService(ActionService actionService)
    {
        this.actionService = actionService;
    }
    public void setWorkflowService(WorkflowService workflowService)
    {
        this.workflowService = workflowService;
    }
    public void setAssetService(AssetService assetService)
    {
        this.assetService = assetService;
    }
    
    public void setTransactionService(TransactionService transactionService)
    {
        this.transactionService = transactionService;
    }
    
 
    /* (non-Javadoc)
     * @see org.alfresco.wcm.sandbox.SandboxService#createAuthorSandbox(java.lang.String)
     */
    public SandboxInfo createAuthorSandbox(String wpStoreId)
    {
        ParameterCheck.mandatoryString("wpStoreId", wpStoreId);
        
        String currentUserName = AuthenticationUtil.getRunAsUser();
        SandboxInfo sbInfo = null;
        
        if (! wpService.isWebUser(wpStoreId, currentUserName))
        {
            throw new AccessDeniedException("Only web project users may create their own (author) sandbox for '"+currentUserName+"' (store id: "+wpStoreId+")");
        }
        else
        {
            sbInfo = createAuthorSandboxImpl(wpStoreId, currentUserName);
        }
        
        return sbInfo;
    }
    /* (non-Javadoc)
     * @see org.alfresco.wcm.sandbox.SandboxService#createAuthorSandbox(java.lang.String, java.lang.String)
     */
    public SandboxInfo createAuthorSandbox(String wpStoreId, String userName)
    {
        ParameterCheck.mandatoryString("wpStoreId", wpStoreId);
        ParameterCheck.mandatoryString("userName", userName);
        
        // is the current user a content manager for this web project ?
        if (! wpService.isContentManager(wpStoreId))
        {
            throw new AccessDeniedException("Only content managers may create author sandbox for '"+userName+"' (store id: "+wpStoreId+")");
        }
        
        return createAuthorSandboxImpl(wpStoreId, userName);
    }
    
    private SandboxInfo createAuthorSandboxImpl(String wpStoreId, String userName)
    {
        WebProjectInfo wpInfo = wpService.getWebProject(wpStoreId);
        
        final NodeRef wpNodeRef = wpInfo.getNodeRef();
        final List managers = new ArrayList(4);
        
        AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork()
        {
            public Object doWork() throws Exception
            {
                // retrieve the list of managers from the existing users
                Map existingUserRoles = wpService.listWebUsers(wpNodeRef);
                for (Map.Entry userRole : existingUserRoles.entrySet())
                {
                    String username = userRole.getKey();
                    String userrole = userRole.getValue();
                      
                    if (WCMUtil.ROLE_CONTENT_MANAGER.equals(userrole) && managers.contains(username) == false)
                    {
                        managers.add(username);
                    }
                }
                return null;
            }
        }, AuthenticationUtil.getSystemUserName());
        
        String role = wpService.getWebUserRole(wpNodeRef, userName);
        SandboxInfo sbInfo = sandboxFactory.createUserSandbox(wpStoreId, managers, userName, role);
        
        List sandboxInfoList = new LinkedList();
        sandboxInfoList.add(sbInfo);
        
        // Bind the post-commit transaction listener with data required for virtualization server notification
        CreateSandboxTransactionListener tl = new CreateSandboxTransactionListener(sandboxInfoList, wpService.listWebApps(wpNodeRef));
        AlfrescoTransactionSupport.bindListener(tl);
        
        if (logger.isInfoEnabled())
        {
           logger.info("Created author sandbox: " + sbInfo.getSandboxId() + " (web project id: " + wpStoreId + ")");
        }
        
        return sbInfo;
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.sandbox.SandboxService#listSandboxes(java.lang.String)
     */
    public List listSandboxes(String wpStoreId)
    {
        ParameterCheck.mandatoryString("wpStoreId", wpStoreId);
        
        String currentUser = AuthenticationUtil.getRunAsUser();
        
        List sbInfos = null;
        
        if (wpService.isContentManager(wpStoreId, currentUser))
        {
            sbInfos = sandboxFactory.listAllSandboxes(wpStoreId);
        }
        else
        {
            sbInfos = new ArrayList(1);
            
            SandboxInfo authorSandbox = getAuthorSandbox(wpStoreId, currentUser);
            
            if (authorSandbox != null)
            {
                sbInfos.add(authorSandbox);
            }
            
            sbInfos.add(getSandbox(WCMUtil.buildStagingStoreName(wpStoreId))); // get staging sandbox
        }
        
        return sbInfos;
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.sandbox.SandboxService#listSandboxes(java.lang.String, java.lang.String)
     */
    public List listSandboxes(final String wpStoreId, String userName)
    {
        ParameterCheck.mandatoryString("wpStoreId", wpStoreId);
        ParameterCheck.mandatoryString("userName", userName);
        
        if (! wpService.isContentManager(wpStoreId))
        {
            throw new AccessDeniedException("Only content managers may list sandboxes for '"+userName+"' (web project id: "+wpStoreId+")");
        }
       
        return AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork>()
        {
             public List doWork() throws Exception
             {
                 return listSandboxes(wpStoreId);
             }
        }, userName);
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.sandbox.SandboxService#isSandboxType(java.lang.String, org.alfresco.service.namespace.QName)
     */
    public boolean isSandboxType(String sbStoreId, QName sandboxType)
    {
        ParameterCheck.mandatoryString("sbStoreId", sbStoreId);
        ParameterCheck.mandatory("sandboxType", sandboxType);
        
        SandboxInfo sbInfo = getSandbox(sbStoreId);
        if (sbInfo != null)
        {
            return sbInfo.getSandboxType().equals(sandboxType);
        }
        return false;
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.sandbox.SandboxService#getSandbox(java.lang.String)
     */
    public SandboxInfo getSandbox(String sbStoreId)
    {
        ParameterCheck.mandatoryString("sbStoreId", sbStoreId);
        
        String wpStoreId = WCMUtil.getWebProjectStoreId(sbStoreId);
        
        // check user has read access to web project (ie. is a web user)
        if (! wpService.isWebUser(wpStoreId))
        {
            return null;
        }
        
        if (! WCMUtil.isStagingStore(sbStoreId))
        {
            String currentUser = AuthenticationUtil.getRunAsUser();
            
            if (! ((WCMUtil.getUserName(sbStoreId).equals(currentUser)) || (wpService.isContentManager(wpStoreId, currentUser))))
            {
                throw new AccessDeniedException("Only content managers may get sandbox '"+sbStoreId+"' (web project id: "+wpStoreId+")");
            }
        }
        
        return sandboxFactory.getSandbox(sbStoreId);
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.sandbox.SandboxService#getAuthorSandbox(java.lang.String)
     */
    public SandboxInfo getAuthorSandbox(String wpStoreId)
    {
        ParameterCheck.mandatoryString("wpStoreId", wpStoreId);
        
        String currentUserName = AuthenticationUtil.getRunAsUser();
        return getSandbox(WCMUtil.buildUserMainStoreName(WCMUtil.buildStagingStoreName(wpStoreId), currentUserName));
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.sandbox.SandboxService#getUserSandbox(java.lang.String, java.lang.String)
     */
    public SandboxInfo getAuthorSandbox(String wpStoreId, String userName)
    {
        ParameterCheck.mandatoryString("wpStoreId", wpStoreId);
        ParameterCheck.mandatoryString("userName", userName);
        
        return getSandbox(WCMUtil.buildUserMainStoreName(WCMUtil.buildStagingStoreName(wpStoreId), userName));
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.sandbox.SandboxService#getStagingSandbox(java.lang.String)
     */
    public SandboxInfo getStagingSandbox(String wpStoreId)
    {
        ParameterCheck.mandatoryString("wpStoreId", wpStoreId);
        
        return getSandbox(WCMUtil.buildStagingStoreName(wpStoreId));
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.sandbox.SandboxService#deleteSandbox(java.lang.String)
     */
    public void deleteSandbox(String sbStoreId)
    {
        ParameterCheck.mandatoryString("sbStoreId", sbStoreId);
        
        String wpStoreId = WCMUtil.getWebProjectStoreId(sbStoreId);
        
        String currentUserName = AuthenticationUtil.getRunAsUser();
        if (sbStoreId.equals(WCMUtil.buildUserMainStoreName(wpStoreId, currentUserName)))
        {
            // author may delete their own sandbox
            sandboxFactory.deleteSandbox(sbStoreId);
        }
        else
        {       
            if (! wpService.isContentManager(wpStoreId))
            {
                throw new AccessDeniedException("Only content managers may delete sandbox '"+sbStoreId+"' (web project id: "+wpStoreId+")");
            }
            
            if (sbStoreId.equals(wpStoreId))
            {
                throw new AccessDeniedException("Cannot delete staging sandbox '"+sbStoreId+"' (web project id: "+wpStoreId+")");
            }
            
            // content manager may delete sandboxes, except staging sandbox
            sandboxFactory.deleteSandbox(sbStoreId);
        }
        
        if (logger.isInfoEnabled())
        {
           logger.info("Deleted sandbox: " + sbStoreId + " (web project id: " + wpStoreId + ")");
        }
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.sandbox.SandboxService#listChangedAll(java.lang.String, boolean)
     */
    public List listChangedAll(String sbStoreId, boolean includeDeleted)
    {
        ParameterCheck.mandatoryString("sbStoreId", sbStoreId);
        
        String avmDirectoryPath = WCMUtil.buildSandboxRootPath(sbStoreId); // currently :/www/avm_webapps
        return listChanged(sbStoreId, WCMUtil.getStoreRelativePath(avmDirectoryPath), includeDeleted);
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.sandbox.SandboxService#listChangedWebApp(java.lang.String, java.lang.String, boolean)
     */
    public List listChangedWebApp(String sbStoreId, String webApp, boolean includeDeleted)
    {
        ParameterCheck.mandatoryString("sbStoreId", sbStoreId);
        ParameterCheck.mandatoryString("webApp", webApp);
        
        // filter by current webapp
        String avmDirectoryPath = WCMUtil.buildStoreWebappPath(sbStoreId, webApp);
        return listChanged(sbStoreId, WCMUtil.getStoreRelativePath(avmDirectoryPath), includeDeleted);
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.sandbox.SandboxService#listChanged(java.lang.String, java.lang.String, boolean)
     */
    public List listChanged(String sbStoreId, String relativePath, boolean includeDeleted)
    {
        ParameterCheck.mandatoryString("sbStoreId", sbStoreId);
        ParameterCheck.mandatoryString("relativePath", relativePath);
        
        // TODO - allow list for any sandbox
        if (! WCMUtil.isUserStore(sbStoreId))
        {
            throw new AlfrescoRuntimeException("Not an author sandbox: "+sbStoreId);
        }
        
        // build the paths to the stores to compare - filter by given directory path
        String wpStoreId = WCMUtil.getWebProjectStoreId(sbStoreId);
        String stagingSandboxId = WCMUtil.buildStagingStoreName(wpStoreId);
        
        return listChanged(sbStoreId, relativePath, stagingSandboxId, relativePath, includeDeleted);
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.sandbox.SandboxService#listChanged(java.lang.String, java.lang.String, java.lang.String, java.lang.String, boolean)
     */
    public List listChanged(String srcSandboxStoreId, String srcRelativePath, String dstSandboxStoreId, String dstRelativePath, boolean includeDeleted)
    {
        ParameterCheck.mandatoryString("srcSandboxStoreId", srcSandboxStoreId);
        ParameterCheck.mandatoryString("srcRelativePath", srcRelativePath);
        
        ParameterCheck.mandatoryString("dstSandboxStoreId", dstSandboxStoreId);
        ParameterCheck.mandatoryString("dstRelativePath", dstRelativePath);
        
        // checks sandbox access (TODO review)
        getSandbox(srcSandboxStoreId); // ignore result
        getSandbox(dstSandboxStoreId); // ignore result
        
        String avmSrcPath = srcSandboxStoreId + WCMUtil.AVM_STORE_SEPARATOR + srcRelativePath;
        String avmDstPath = dstSandboxStoreId + WCMUtil.AVM_STORE_SEPARATOR + dstRelativePath;
        
        return listChanged(-1, avmSrcPath, -1, avmDstPath, includeDeleted);
    }
    
    private List listChanged(int srcVersion, String srcPath, int dstVersion, String dstPath, boolean includeDeleted)
    {
        long start = System.currentTimeMillis();
        
        List diffs = avmSyncService.compare(srcVersion, srcPath, dstVersion, dstPath, nameMatcher);
        
        List assets = new ArrayList(diffs.size());
        
        for (AVMDifference diff : diffs)
        {
            // convert each diff record into an AVM node descriptor
            String sourcePath = diff.getSourcePath();
            
            String[] parts = WCMUtil.splitPath(sourcePath);
            AssetInfo asset = assetService.getAsset(parts[0], -1, parts[1], includeDeleted);
            if (asset != null)
            {
                // TODO refactor
                ((AssetInfoImpl)asset).setDiffCode(diff.getDifferenceCode());
                assets.add(asset);
            }
        }
        
        if (logger.isTraceEnabled())
        {
            logger.trace("listChanged: "+assets.size()+" assets in "+(System.currentTimeMillis()-start)+" ms (between "+srcVersion+","+srcPath+" and "+dstVersion+","+dstPath);
        }
        return assets;
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.sandbox.SandboxService#submitAll(java.lang.String, java.lang.String, java.lang.String)
     */
    public void submitAll(String sbStoreId, String submitLabel, String submitComment)
    {
        ParameterCheck.mandatoryString("sbStoreId", sbStoreId);
        
        String avmDirectoryPath = WCMUtil.buildSandboxRootPath(sbStoreId); // currently :/www/avm_webapps
        submit(sbStoreId, WCMUtil.getStoreRelativePath(avmDirectoryPath), submitLabel, submitComment);
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.sandbox.SandboxService#submitWebApp(java.lang.String, java.lang.String, java.lang.String, java.lang.String)
     */
    public void submitWebApp(String sbStoreId, String webApp, String submitLabel, String submitComment)
    {
        ParameterCheck.mandatoryString("sbStoreId", sbStoreId);
        ParameterCheck.mandatoryString("webApp", webApp);
        
        String avmDirectoryPath = WCMUtil.buildStoreWebappPath(sbStoreId, webApp);
        submit(sbStoreId, WCMUtil.getStoreRelativePath(avmDirectoryPath), submitLabel, submitComment);
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.sandbox.SandboxService#submit(java.lang.String, java.lang.String, java.lang.String, java.lang.String)
     */
    public void submit(String sbStoreId, String relativePath, String submitLabel, String submitComment)
    {
        ParameterCheck.mandatoryString("sbStoreId", sbStoreId);
        ParameterCheck.mandatoryString("relativePath", relativePath);
        
        List assets = listChanged(sbStoreId, relativePath, true);
        
        submitListAssets(sbStoreId, assets, submitLabel, submitComment);
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.sandbox.SandboxService#submitList(java.lang.String, java.util.List, java.lang.String, java.lang.String)
     */
    public void submitList(String sbStoreId, List relativePaths, String submitLabel, String submitComment)
    {
        ParameterCheck.mandatoryString("sbStoreId", sbStoreId);
        
        List assets = new ArrayList(relativePaths.size());
        
        for (String relativePath : relativePaths)
        {
            // convert each path into an asset
            AssetInfo asset = assetService.getAsset(sbStoreId, -1, relativePath, true);
            if (asset != null)
            {
                assets.add(asset);
            }
        }
        
        submitListAssets(sbStoreId, assets, submitLabel, submitComment);
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.sandbox.SandboxService#submitListAssets(java.lang.String, java.util.List, java.lang.String, java.lang.String)
     */
    public void submitListAssets(String sbStoreId, List assets, String submitLabel, String submitComment)
    {
        ParameterCheck.mandatoryString("sbStoreId", sbStoreId);
        ParameterCheck.mandatoryString("submitLabel", submitLabel);
        
        // TODO - consider submit to higher-level sandbox, not just to staging
        if (! WCMUtil.isUserStore(sbStoreId))
        {
            throw new AlfrescoRuntimeException("Not an author sandbox: "+sbStoreId);
        }
        
        List relativePaths = new ArrayList(assets.size());
        for (AssetInfo asset : assets)
        {
            relativePaths.add(asset.getPath());
        }
        
        // via submit direct workflow
        submitViaWorkflow(sbStoreId, relativePaths, null, null, submitLabel, submitComment, null, null, false, false);
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.sandbox.SandboxService#submitListAssets(java.lang.String, java.util.List, java.lang.String, java.util.Map, java.lang.String, java.lang.String, java.util.Map, java.util.Date, boolean, boolean)
     */
    public void submitListAssets(String sbStoreId, List relativePaths,
                                 String workflowName, Map workflowParams, 
                                 String submitLabel, String submitComment,
                                 Map expirationDates, Date launchDate, boolean validateLinks, boolean autoDeploy)
    {
        // via selected workflow
        submitViaWorkflow(sbStoreId, relativePaths, workflowName, workflowParams, submitLabel, submitComment,
                          expirationDates, launchDate, validateLinks, autoDeploy);
    }
    
    /**
     * Submits the selected items via the configured workflow.
     * 
     * This method uses 2 separate transactions to perform the submit.
     * The first one creates the workflow sandbox. The virtualisation
     * server is then informed of the new stores. The second
     * transaction then starts the appropriate workflow. This approach
     * is needed to allow link validation to be performed on the
     * workflow sandbox.
     */
    private void submitViaWorkflow(final String sbStoreId, final List relativePaths, String workflowName, Map workflowParams, 
                                   final String submitLabel, final String submitComment,
                                   final Map expirationDates, final Date launchDate, final boolean validateLinks, final boolean autoDeploy)
    {
        // checks sandbox access (TODO review)
        getSandbox(sbStoreId); // ignore result
        
        final String wpStoreId = WCMUtil.getWebProjectStoreId(sbStoreId);
        final String stagingSandboxId = WCMUtil.buildStagingStoreName(wpStoreId);
        
        final String finalWorkflowName;
        final Map finalWorkflowParams;
        
        if ((workflowName == null) || (workflowName.equals("")))
        {
            finalWorkflowName = WORKFLOW_SUBMITDIRECT;
            finalWorkflowParams = new HashMap();
        }
        else
        {
            finalWorkflowName = workflowName;
            finalWorkflowParams = workflowParams;
        }
        
        RetryingTransactionHelper txnHelper = transactionService.getRetryingTransactionHelper();
        
        final List srcPaths = new ArrayList(relativePaths.size());
        String derivedWebApp = null;
        boolean multiWebAppsFound = false;
        
        for (String relativePath : relativePaths)
        {
            // Example srcPath:
            //     mysite--alice:/www/avm_webapps/ROOT/foo.txt
            String srcPath = sbStoreId + WCMUtil.AVM_STORE_SEPARATOR + relativePath;
            srcPaths.add(srcPath);
           
            // derive webapp for now (TODO check usage)
            String srcWebApp = WCMUtil.getWebapp(srcPath);
            if (srcWebApp != null)
            {
                if (derivedWebApp == null)
                {
                    derivedWebApp = srcWebApp;
                }
                else if (! derivedWebApp.equals(srcWebApp))
                {
                    multiWebAppsFound = true;
                }
            }
        }
        
        final String webApp = (multiWebAppsFound == false ? derivedWebApp : null);
       
        RetryingTransactionCallback> sandboxCallback = new RetryingTransactionCallback>()
        {
            public Pair execute() throws Throwable
            {
                // call the actual implementation
                return createWorkflowSandbox(finalWorkflowName, finalWorkflowParams, stagingSandboxId, srcPaths, expirationDates);
            }
        };
        // create the workflow sandbox firstly
        final Pair workflowInfo = txnHelper.doInTransaction(sandboxCallback, false, true);
        if (workflowInfo != null)
        {
            final SandboxInfo wfSandboxInfo = workflowInfo.getFirst();
            String virtUpdatePath = workflowInfo.getSecond();
            
            // inform the virtualisation server if the workflow sandbox was created
            if (virtUpdatePath != null)
            {
                WCMUtil.updateVServerWebapp(virtServerRegistry, virtUpdatePath, true);
            }
            try
            {
                RetryingTransactionCallback workflowCallback = new RetryingTransactionCallback()
                {
                    public String execute() throws Throwable
                    {
                        // call the actual implementation
                        startWorkflow(wpStoreId, sbStoreId, wfSandboxInfo, webApp, finalWorkflowName, finalWorkflowParams, submitLabel, submitComment, launchDate, validateLinks, autoDeploy);
                        return null;
                    }
                };
                
                // start the workflow
                txnHelper.doInTransaction(workflowCallback, false, true);
            }
            catch (Throwable err)
            {
                cleanupWorkflowSandbox(wfSandboxInfo);
                throw new AlfrescoRuntimeException("Failed to submit to workflow", err);
            }
        }
    }
    
    /**
     * Creates a workflow sandbox for all the submitted items
     *
     * @param context Faces context
     */
    protected Pair createWorkflowSandbox(String workflowName, Map workflowParams, String stagingSandboxId, final List srcPaths, Map expirationDates)
    {
        // The virtualization server might need to be notified
        // because one or more of the files submitted could alter
        // the behavior the virtual webapp in the target of the submit.
        // For example, the user might be submitting a new jar or web.xml file.
        //
        // This must take place after the transaction has been completed;
        // therefore, a variable is needed to store the path to the
        // updated webapp so it can happen in doPostCommitProcessing.
        String virtUpdatePath = null;
        SandboxInfo sandboxInfo = null;
       
        // create container for our avm workflow package
       
        if (! workflowName.equals(WORKFLOW_SUBMITDIRECT))
        {
            // Create workflow sandbox for workflow package
            sandboxInfo = sandboxFactory.createWorkflowSandbox(stagingSandboxId);
        }
        else
        {
            // default to direct submit workflow
            // NOTE: read only workflow sandbox is lighter to construct than full workflow sandbox
            sandboxInfo = sandboxFactory.createReadOnlyWorkflowSandbox(stagingSandboxId);
        }
       
        // Example workflow main store name:
        //     mysite--workflow-9161f640-b020-11db-8015-130bf9b5b652
        String workflowMainStoreName = sandboxInfo.getMainStoreName();
       
        List diffs = new ArrayList(srcPaths.size());
       
        // get diff list - also process expiration dates, if any, and set virt svr update path
       
        for (String srcPath : srcPaths)
        {
            // We *always* want to update virtualization server
            // when a workflow sandbox is given data in the
            // context of a submit workflow.  Without this,
            // it would be impossible to see workflow data
            // in context.  The raw operation to create a
            // workflow sandbox does not notify the virtualization
            // server that it exists because it's useful to
            // defer this operation until everything is already
            // in place; this allows pointlessly fine-grained
            // notifications to be suppressed (they're expensive).
            //
            // Therefore, just derive the name of the webapp
            // in the workflow sandbox from the 1st item in
            // the submit list (even if it's not in WEB-INF),
            // and force the virt server notification after the
            // transaction has completed via doPostCommitProcessing.
            if (virtUpdatePath  == null)
            {
                // The virtUpdatePath looks just like the srcPath
                // except that it belongs to a the main store of
                // the workflow sandbox instead of the sandbox
                // that originated the submit.
                virtUpdatePath = workflowMainStoreName + srcPath.substring(srcPath.indexOf(':'),srcPath.length());
            }
            if ((expirationDates != null) && (! expirationDates.isEmpty()))
            {
                // process the expiration date (if any)
                processExpirationDate(srcPath, expirationDates);
            }
          
            diffs.add(new AVMDifference(-1, srcPath, -1, WCMUtil.getCorrespondingPath(srcPath, workflowMainStoreName), AVMDifference.NEWER));
        }
        // write changes to layer so files are marked as modified
        avmSyncService.update(diffs, null, false, false, false, false, null, null);
       
        return new Pair(sandboxInfo, virtUpdatePath);
    }
    /**
     * Starts the configured workflow to allow the submitted items to be link
     * checked and reviewed.
     */
    protected void startWorkflow(String wpStoreId, String sbStoreId, SandboxInfo wfSandboxInfo, String webApp, String workflowName, Map workflowParams, 
                                 String submitLabel, String submitComment, Date launchDate, boolean validateLinks, boolean autoDeploy)
    {
        ParameterCheck.mandatoryString("workflowName", workflowName);
        ParameterCheck.mandatory("workflowParams", workflowParams);
        
        // start the workflow to get access to the start task
        WorkflowDefinition wfDef = workflowService.getDefinitionByName(workflowName);
        WorkflowPath path = workflowService.startWorkflow(wfDef.id, null);
        
        if (path != null)
        {
            // extract the start task
            List tasks = workflowService.getTasksForWorkflowPath(path.id);
            if (tasks.size() == 1)
            {
                WorkflowTask startTask = tasks.get(0);
                if (startTask.state == WorkflowTaskState.IN_PROGRESS)
                {
                    final NodeRef workflowPackage = WCMWorkflowUtil.createWorkflowPackage(workflowService, avmService, wfSandboxInfo);
                    workflowParams.put(WorkflowModel.ASSOC_PACKAGE, workflowPackage);
                    // add submission parameters
                    workflowParams.put(WorkflowModel.PROP_WORKFLOW_DESCRIPTION, submitComment);
                    workflowParams.put(WCMWorkflowModel.PROP_LABEL, submitLabel);
                    workflowParams.put(WCMWorkflowModel.PROP_FROM_PATH,
                            WCMUtil.buildStoreRootPath(sbStoreId));
                    workflowParams.put(WCMWorkflowModel.PROP_LAUNCH_DATE, launchDate);
                    workflowParams.put(WCMWorkflowModel.PROP_VALIDATE_LINKS,
                            new Boolean(validateLinks));
                    workflowParams.put(WCMWorkflowModel.PROP_AUTO_DEPLOY,
                            new Boolean(autoDeploy));
                    workflowParams.put(WCMWorkflowModel.PROP_WEBAPP,
                            webApp);
                    workflowParams.put(WCMWorkflowModel.ASSOC_WEBPROJECT,
                           wpService.getWebProjectNodeFromStore(wpStoreId));
                    // update start task with submit parameters
                    workflowService.updateTask(startTask.id, workflowParams, null, null);
                    // end the start task to trigger the first 'proper' task in the workflow
                    workflowService.endTask(startTask.id, null);
                }
            }
        }
    }
    /**
     * Cleans up the workflow sandbox created by the first transaction. This
     * action is itself preformed in a separate transaction.
     */
    private void cleanupWorkflowSandbox(final SandboxInfo sandboxInfo)
    {
        RetryingTransactionHelper txnHelper = transactionService.getRetryingTransactionHelper();
       
        RetryingTransactionCallback callback = new RetryingTransactionCallback()
        {
            public String execute() throws Throwable
            {
                // call the actual implementation
                cleanupWorkflowSandboxImpl(sandboxInfo);
                return null;
            }
        };
        try
        {
            // Execute the cleanup handler
            txnHelper.doInTransaction(callback);
        }
        catch (Throwable e)
        {
            // not much we can do now, just log the error to inform admins
            logger.error("Failed to cleanup workflow sandbox after workflow failure", e);
        }
    }
    /**
     * Performs the actual deletion of stores in the workflow sandbox.
     */
    private void cleanupWorkflowSandboxImpl(SandboxInfo sandboxInfo)
    {
        if (sandboxInfo != null)
        {
            String mainWorkflowStore = sandboxInfo.getMainStoreName();
            Map matches = avmService.queryStorePropertyKey(mainWorkflowStore, 
                                                                                 QName.createQName(null, ".sandbox-id%"));
            
            QName sandboxID = matches.keySet().iterator().next();
            // Get all the stores in the sandbox.
            Map> stores = avmService.queryStoresPropertyKeys(sandboxID);
            for (String storeName : stores.keySet())
            {
                avmService.purgeStore(storeName);
            }
        }
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.sandbox.SandboxService#revertAll(java.lang.String)
     */  
    public void revertAll(String sbStoreId)
    {
        ParameterCheck.mandatoryString("sbStoreId", sbStoreId);
        
        String avmDirectoryPath = WCMUtil.buildSandboxRootPath(sbStoreId); // currently :/www/avm_webapps
        revert(sbStoreId, WCMUtil.getStoreRelativePath(avmDirectoryPath));
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.sandbox.SandboxService#revertWebApp(java.lang.String, java.lang.String)
     */
    public void revertWebApp(String sbStoreId, String webApp)
    {
        ParameterCheck.mandatoryString("sbStoreId", sbStoreId);
        ParameterCheck.mandatoryString("webApp", webApp);
        
        String avmDirectoryPath = WCMUtil.buildStoreWebappPath(sbStoreId, webApp);
        revert(sbStoreId, WCMUtil.getStoreRelativePath(avmDirectoryPath));
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.sandbox.SandboxService#revertAllDir(java.lang.String, java.lang.String)
     */
    public void revert(String sbStoreId, String relativePath)
    {
        ParameterCheck.mandatoryString("sbStoreId", sbStoreId);
        ParameterCheck.mandatoryString("relativePath", relativePath);
        
        List assets = listChanged(sbStoreId, relativePath, true);
        
        revertListAssets(sbStoreId, assets);
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.sandbox.SandboxService#revertList(java.lang.String, java.util.List)
     */
    public void revertList(String sbStoreId, List relativePaths)
    {
        ParameterCheck.mandatoryString("sbStoreId", sbStoreId);
        
        List assets = new ArrayList(relativePaths.size());
        
        for (String relativePath : relativePaths)
        {
            // convert each path into an asset
            AssetInfo asset = assetService.getAsset(sbStoreId, -1, relativePath, true);
            if (asset != null)
            {
                assets.add(asset);
            }
        }
        
        revertListAssets(sbStoreId, assets);
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.sandbox.SandboxService#revertListAssets(java.lang.String, java.util.List)
     */
    public void revertListAssets(String sbStoreId, List assets)
    {
        ParameterCheck.mandatoryString("sbStoreId", sbStoreId);
        
        // checks sandbox access (TODO review)
        getSandbox(sbStoreId); // ignore result
        
        List> versionPaths = new ArrayList>(assets.size());
        
        List tasks = null;
        for (AssetInfo asset : assets)
        {
           if (tasks == null)
           {
              tasks = WCMWorkflowUtil.getAssociatedTasksForSandbox(workflowService, WCMUtil.getSandboxStoreId(asset.getAvmPath()));
           }
           
           // TODO ... extra lookup ... either return AVMNodeDescriptor or change getAssociatedTasksForNode ...
           AVMNodeDescriptor node = avmService.lookup(-1, asset.getAvmPath());
           
           if (WCMWorkflowUtil.getAssociatedTasksForNode(avmService, node, tasks).size() == 0)
           {
              String revertPath = asset.getAvmPath();
              versionPaths.add(new Pair(-1, revertPath));
              
              if (VirtServerUtils.requiresUpdateNotification(revertPath))
              {
                  // Bind the post-commit transaction listener with data required for virtualization server notification
                  UpdateSandboxTransactionListener tl = new UpdateSandboxTransactionListener(revertPath);
                  AlfrescoTransactionSupport.bindListener(tl);
              }
           }
        }
        
        Map args = new HashMap(1, 1.0f);
        args.put(AVMUndoSandboxListAction.PARAM_NODE_LIST, (Serializable)versionPaths);
        Action action = actionService.createAction(AVMUndoSandboxListAction.NAME, args);
        actionService.executeAction(action, null);    // dummy action ref, list passed as action arg
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.sandbox.SandboxService#listSnapshots(java.lang.String, boolean)
     */
    public List listSnapshots(String sbStoreId, boolean includeSystemGenerated)
    {
        ParameterCheck.mandatoryString("sbStoreId", sbStoreId);
        
        String wpStoreId = WCMUtil.getWebProjectStoreId(sbStoreId);
        if (! wpService.isContentManager(wpStoreId))
        {
            throw new AccessDeniedException("Only content managers may list snapshots '"+sbStoreId+"' (web project id: "+wpStoreId+")");
        }
        
        List allVersions = avmService.getStoreVersions(sbStoreId);
        return listSnapshots(allVersions, includeSystemGenerated);
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.sandbox.SandboxService#listSnapshots(java.lang.String, java.util.Date, java.util.Date, boolean)
     */
    public List listSnapshots(String sbStoreId, Date from, Date to, boolean includeSystemGenerated)
    {
        ParameterCheck.mandatoryString("sbStoreId", sbStoreId);
        
        String wpStoreId = WCMUtil.getWebProjectStoreId(sbStoreId);
        if (! wpService.isContentManager(wpStoreId))
        {
            throw new AccessDeniedException("Only content managers may list snapshots '"+sbStoreId+"' (web project id: "+wpStoreId+")");
        }
        
        List versionsToFilter = avmService.getStoreVersions(sbStoreId, from, to);
        return listSnapshots(versionsToFilter, includeSystemGenerated);
    }
        
    private List listSnapshots(List versionsToFilter, boolean includeSystemGenerated)
    {
        List versions = new ArrayList(versionsToFilter.size());
        
        for (int i = versionsToFilter.size() - 1; i >= 0; i--) // reverse order
        {
            VersionDescriptor item = versionsToFilter.get(i);
            // only display snapshots with a valid tag - others are system generated snapshots
            if ((includeSystemGenerated == true) || ((item.getTag() != null) && (item.getVersionID() != 0)))
            {
                versions.add(new SandboxVersionImpl(item));
            }
        }
        
        return versions;
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.sandbox.SandboxService#revertSnapshot(java.lang.String, int)
     */
    public void revertSnapshot(final String sbStoreId, final int version)
    {
        ParameterCheck.mandatoryString("sbStoreId", sbStoreId);
                
        String wpStoreId = WCMUtil.getWebProjectStoreId(sbStoreId);
        if (! wpService.isContentManager(wpStoreId))
        {
            throw new AccessDeniedException("Only content managers may revert staging sandbox '"+sbStoreId+"' (web project id: "+wpStoreId+")");
        }
        // do this as system as the staging area has restricted access (and content manager may not have permission to delete children, for example)
        List diffs = AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork>()
        {
            public List doWork() throws Exception
            {
                String sandboxPath = WCMUtil.buildSandboxRootPath(sbStoreId);
                List diffs = avmSyncService.compare(-1, sandboxPath, version, sandboxPath, null);
                Map args = new HashMap(1, 1.0f);
                args.put(AVMRevertStoreAction.PARAM_VERSION, version);
                Action action = actionService.createAction(AVMRevertStoreAction.NAME, args);
                actionService.executeAction(action, AVMNodeConverter.ToNodeRef(-1, sbStoreId + WCMUtil.AVM_STORE_SEPARATOR + "/"));
                return diffs;
            }
        }, AuthenticationUtil.getSystemUserName());
         
        // See if any of the files being reverted require notification of the virt server, to update the webapp
        for (AVMDifference diff : diffs)
        {
            if (VirtServerUtils.requiresUpdateNotification(diff.getSourcePath()))
            {
                // Bind the post-commit transaction listener with data required for virtualization server notification
                UpdateSandboxTransactionListener tl = new UpdateSandboxTransactionListener(diff.getSourcePath());
                AlfrescoTransactionSupport.bindListener(tl);
                break;
            }
        }
    }
    
    /**
     * Sets up the expiration date for the given source path
     *
     * @param srcPath The path to set the expiration date for
     */
    private void processExpirationDate(String srcPath, Map expirationDates)
    {
       // if an expiration date has been set for this item we need to
       // add the expires aspect and the date supplied
       Date expirationDate = expirationDates.get(srcPath);
       if (expirationDate == null)
       {
          return;
       }
       
       // make sure the aspect is present
       if (avmService.hasAspect(-1, srcPath, WCMAppModel.ASPECT_EXPIRES) == false)
       {
           avmService.addAspect(srcPath, WCMAppModel.ASPECT_EXPIRES);
       }
       // set the expiration date
       avmService.setNodeProperty(srcPath, WCMAppModel.PROP_EXPIRATIONDATE, 
                                       new PropertyValue(DataTypeDefinition.DATETIME, expirationDate));
       if (logger.isDebugEnabled())
       {
           logger.debug("Set expiration date of " + expirationDate + " for " + srcPath);
       }
    }
    
    /**
     * Create Sandbox Transaction listener - invoked after commit
     */
    private class CreateSandboxTransactionListener extends TransactionListenerAdapter
    {
        private List sandboxInfoList;
        private List webAppNames;
        
        public CreateSandboxTransactionListener(List sandboxInfoList, List webAppNames)
        {
            this.sandboxInfoList = sandboxInfoList;
            this.webAppNames = webAppNames;
        }
        /**
         * @see org.alfresco.repo.transaction.TransactionListenerAdapter#afterCommit()
         */
        @Override
        public void afterCommit()
        {
            // Handle notification to the virtualization server 
            // (this needs to occur after the sandboxes are created in the main txn)
            
            // reload virtualisation server for webapp(s) in this web project
            for (SandboxInfo sandboxInfo : this.sandboxInfoList)
            {
                String newlyInvitedStoreName = WCMUtil.buildStagingStoreName(sandboxInfo.getMainStoreName());
                
                for (String webAppName : webAppNames)
                {
                    String path = WCMUtil.buildStoreWebappPath(newlyInvitedStoreName, webAppName);
                    WCMUtil.updateVServerWebapp(virtServerRegistry, path, true);
                }
            }
        }
    }
    
    /**
     * Update Sandbox Transaction listener - invoked after submit or revert
     */
    private class UpdateSandboxTransactionListener extends TransactionListenerAdapter
    {
        private String virtUpdatePath;
        
        public UpdateSandboxTransactionListener(String virtUpdatePath)
        {
            this.virtUpdatePath = virtUpdatePath;
        }
        /**
         * @see org.alfresco.repo.transaction.TransactionListenerAdapter#afterCommit()
         */
        @Override
        public void afterCommit()
        {
            // The virtualization server might need to be notified
            // because one or more of the files submitted / reverted could alter
            // the behavior the virtual webapp in the target of the submit.
            // For example, the user might be submitting a new jar or web.xml file.
            //
            // This must take place after the transaction has been completed;
            
            // force an update of the virt server if necessary
            if (this.virtUpdatePath != null)
            {
               WCMUtil.updateVServerWebapp(virtServerRegistry, this.virtUpdatePath, true);
            }
        }
    }
}