/*
 * Copyright (C) 2005-2008 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.asset;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.mbeans.VirtServerRegistry;
import org.alfresco.model.ApplicationModel;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.action.executer.ImporterActionExecuter;
import org.alfresco.repo.avm.AVMNodeConverter;
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.TransactionListenerAdapter;
import org.alfresco.service.cmr.avm.AVMNodeDescriptor;
import org.alfresco.service.cmr.avm.AVMService;
import org.alfresco.service.cmr.avm.locking.AVMLock;
import org.alfresco.service.cmr.avm.locking.AVMLockingService;
import org.alfresco.service.cmr.model.FileExistsException;
import org.alfresco.service.cmr.repository.ContentReader;
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.namespace.QName;
import org.alfresco.util.ParameterCheck;
import org.alfresco.util.TempFileProvider;
import org.alfresco.wcm.sandbox.SandboxConstants;
import org.alfresco.wcm.util.WCMUtil;
import org.apache.tools.zip.ZipFile;
/**
 * Asset Service fundamental API.
 * 
 * This service API is designed to support the public facing Asset APIs. 
 * 
 * @author janv
 */
public class AssetServiceImpl implements AssetService
{
    /** Logger */
    //private static Log logger = LogFactory.getLog(AssetServiceImpl.class);
    
    private static char PATH_SEPARATOR = '/';
    
    private static final int BUFFER_SIZE = 16384;
    
    private AVMService avmService;
    private AVMLockingService avmLockingService;
    private NodeService avmNodeService; // AVM node service (ML-aware)
    private VirtServerRegistry virtServerRegistry;
    
    public void setAvmService(AVMService avmService)
    {
        this.avmService = avmService;
    }
    
    public void setAvmLockingService(AVMLockingService avmLockingService)
    {
        this.avmLockingService = avmLockingService;
    }
    
    public void setNodeService(NodeService avmNodeService)
    {
        this.avmNodeService = avmNodeService;
    }
    
    public void setVirtServerRegistry(VirtServerRegistry virtServerRegistry)
    {
        this.virtServerRegistry = virtServerRegistry;
    }
    
    
    private String addLeadingSlash(String path)
    {
        if (path.charAt(0) != PATH_SEPARATOR)
        {
            path = PATH_SEPARATOR + path;
        }
        
        return path;
    }
    
    private void checkMandatoryPath(String path)
    {
        ParameterCheck.mandatoryString("path", path);
        
        if (path.indexOf(WCMUtil.AVM_STORE_SEPARATOR) != -1)
        {
            throw new IllegalArgumentException("Unexpected path '"+path+"' - should not contain '"+WCMUtil.AVM_STORE_SEPARATOR+"'");
        }
    }
    
    private boolean isWebProjectStagingSandbox(String sbStoreId)
    {
        PropertyValue propVal = avmService.getStoreProperty(sbStoreId, SandboxConstants.PROP_WEB_PROJECT_NODE_REF);
        return ((propVal != null) && (WCMUtil.isStagingStore(sbStoreId)));
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.asset.AssetService#createFolderWebApp(java.lang.String, java.lang.String, java.lang.String, java.lang.String)
     */
    public void createFolderWebApp(String sbStoreId, String webApp, String parentFolderPathRelativeToWebApp, String name)
    {
        ParameterCheck.mandatoryString("sbStoreId", sbStoreId);
        ParameterCheck.mandatoryString("webApp", webApp);
        checkMandatoryPath(parentFolderPathRelativeToWebApp);
        ParameterCheck.mandatoryString("name", name);
        
        if (! isWebProjectStagingSandbox(sbStoreId))
        {
            parentFolderPathRelativeToWebApp = addLeadingSlash(parentFolderPathRelativeToWebApp);
            
            String avmParentPath = WCMUtil.buildStoreWebappPath(sbStoreId, webApp) + parentFolderPathRelativeToWebApp;
            
            createFolderAVM(avmParentPath, name, null);
        }
        else
        {
            throw new AccessDeniedException("Not allowed to write in: " + sbStoreId);
        }
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.asset.AssetService#createFolder(java.lang.String, java.lang.String, java.lang.String, java.util.Map)
     */
    public void createFolder(String sbStoreId, String parentFolderPath, String name, Map properties)
    {
        ParameterCheck.mandatoryString("sbStoreId", sbStoreId);
        ParameterCheck.mandatoryString("parentFolderPath", parentFolderPath);
        ParameterCheck.mandatoryString("name", name);
        
        parentFolderPath = addLeadingSlash(parentFolderPath);
        
        String avmParentPath = sbStoreId + WCMUtil.AVM_STORE_SEPARATOR + parentFolderPath;
        
        createFolderAVM(avmParentPath, name, properties);
    }
    
    private void createFolderAVM(String avmParentPath, String name, Map properties)
    {
        ParameterCheck.mandatoryString("avmParentPath", avmParentPath);
        ParameterCheck.mandatoryString("name", name);
        
        String sbStoreId = WCMUtil.getSandboxStoreId(avmParentPath);
        if (! isWebProjectStagingSandbox(sbStoreId))
        {
            avmService.createDirectory(avmParentPath, name);
            
            String avmPath = avmParentPath + PATH_SEPARATOR + name;
            
            // for WCM Web Client (Alfresco Explorer)
            avmService.addAspect(avmPath, ApplicationModel.ASPECT_UIFACETS);
            
            if ((properties != null) && (properties.size() > 0))
            {
                setProperties(avmPath, properties);
            }
        }
        else
        {
            throw new AccessDeniedException("Not allowed to write in: " + sbStoreId);
        }
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.asset.AssetService#createFileWebApp(java.lang.String, java.lang.String, java.lang.String, java.lang.String)
     */
    public ContentWriter createFileWebApp(String sbStoreId, String webApp, String parentFolderPathRelativeToWebApp, String name)
    {
        ParameterCheck.mandatoryString("sbStoreId", sbStoreId);
        ParameterCheck.mandatoryString("webApp", webApp);
        ParameterCheck.mandatoryString("parentFolderPathRelativeToWebApp", parentFolderPathRelativeToWebApp);
        ParameterCheck.mandatoryString("name", name);
        
        parentFolderPathRelativeToWebApp = addLeadingSlash(parentFolderPathRelativeToWebApp);
        
        String avmParentPath = WCMUtil.buildStoreWebappPath(sbStoreId, webApp) + parentFolderPathRelativeToWebApp;
        
        createFileAVM(avmParentPath, name);
        
        String avmPath = avmParentPath + PATH_SEPARATOR + name;
        
        return avmService.getContentWriter(avmPath);
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.asset.AssetService#createFile(java.lang.String, java.lang.String, java.lang.String, java.util.Map)
     */
    public ContentWriter createFile(String sbStoreId, String parentFolderPath, String name, Map properties)
    {
        ParameterCheck.mandatoryString("sbStoreId", sbStoreId);
        ParameterCheck.mandatoryString("parentFolderPath", parentFolderPath);
        ParameterCheck.mandatoryString("name", name);
        
        parentFolderPath = addLeadingSlash(parentFolderPath);
        
        String avmParentPath = sbStoreId + WCMUtil.AVM_STORE_SEPARATOR + parentFolderPath;
        
        createFileAVM(avmParentPath, name);
        
        String avmPath = avmParentPath + PATH_SEPARATOR + name;
        
        if ((properties != null) && (properties.size() > 0))
        {
            setProperties(avmPath, properties);
        }
        
        return avmService.getContentWriter(avmPath);
    }
    
    private void createFileAVM(String avmParentPath, String name)
    {
        ParameterCheck.mandatoryString("avmParentPath", avmParentPath);
        ParameterCheck.mandatoryString("name", name);
        
        String sbStoreId = WCMUtil.getSandboxStoreId(avmParentPath);
        if (! isWebProjectStagingSandbox(sbStoreId))
        {
            avmService.createFile(avmParentPath, name);            
        }
        else
        {
            throw new AccessDeniedException("Not allowed to write in: " + sbStoreId);
        }
    }
    
    private void createFileAVM(String avmParentPath, String name, InputStream in)
    {
        ParameterCheck.mandatoryString("avmParentPath", avmParentPath);
        
        String sbStoreId = WCMUtil.getSandboxStoreId(avmParentPath);
        if (! isWebProjectStagingSandbox(sbStoreId))
        {
            avmService.createFile(avmParentPath, name,  in, null, null);
        }
        else
        {
            throw new AccessDeniedException("Not allowed to write in: " + sbStoreId);
        }
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.asset.AssetService#getContentWriter(org.alfresco.wcm.asset.AssetInfo)
     */
    public ContentWriter getContentWriter(AssetInfo asset)
    {
        ParameterCheck.mandatory("asset", asset);
        
        if (! isWebProjectStagingSandbox(asset.getSandboxId()))
        {
            return avmService.getContentWriter(asset.getAvmPath());
        }
        else
        {
            throw new AccessDeniedException("Not allowed to write in: " + asset.getSandboxId());
        }
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.asset.AssetService#getContentReader(org.alfresco.wcm.asset.AssetInfo)
     */
    public ContentReader getContentReader(AssetInfo asset)
    {
        ParameterCheck.mandatory("asset", asset);
        
        return avmService.getContentReader(asset.getSandboxVersion(), asset.getAvmPath());
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.asset.AssetService#getAssetWebApp(java.lang.String, java.lang.String, java.lang.String)
     */
    public AssetInfo getAssetWebApp(String sbStoreId, String webApp, String pathRelativeToWebApp)
    {
        return getAssetWebApp(sbStoreId, webApp, pathRelativeToWebApp, false);
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.asset.AssetService#getAssetWebApp(java.lang.String, java.lang.String, java.lang.String, boolean)
     */
    public AssetInfo getAssetWebApp(String sbStoreId, String webApp, String pathRelativeToWebApp, boolean includeDeleted)
    {
        ParameterCheck.mandatoryString("sbStoreId", sbStoreId);
        ParameterCheck.mandatoryString("webApp", webApp);
        ParameterCheck.mandatoryString("pathRelativeToWebApp", pathRelativeToWebApp);
        
        pathRelativeToWebApp = addLeadingSlash(pathRelativeToWebApp);
        
        String avmPath = WCMUtil.buildStoreWebappPath(sbStoreId, webApp) + pathRelativeToWebApp;
        
        return getAssetAVM(-1, avmPath, includeDeleted);
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.asset.AssetService#getAsset(java.lang.String, java.lang.String)
     */
    public AssetInfo getAsset(String sbStoreId, String path)
    {
        return getAsset(sbStoreId, -1, path, false);
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.asset.AssetService#getAsset(java.lang.String, int, java.lang.String, boolean)
     */
    public AssetInfo getAsset(String sbStoreId, int version, String path, boolean includeDeleted)
    {
        ParameterCheck.mandatoryString("sbStoreId", sbStoreId);
        ParameterCheck.mandatoryString("path", path);
        
        path = addLeadingSlash(path);
        
        String avmPath = sbStoreId + WCMUtil.AVM_STORE_SEPARATOR + path;
        
        return getAssetAVM(version, avmPath, includeDeleted);
    }
    
    private AssetInfo getAssetAVM(int version, String avmPath, boolean includeDeleted)
    {
        ParameterCheck.mandatoryString("avmPath", avmPath);
        
        AVMNodeDescriptor node = avmService.lookup(version, avmPath, includeDeleted);
        
        AssetInfo asset = null;
        
        if (node != null)
        {
            String lockOwner = null;
            if (avmLockingService != null)
            {
                String wpStoreId = WCMUtil.getWebProjectStoreIdFromPath(avmPath);
                String[] parts = WCMUtil.splitPath(avmPath);
                lockOwner = getLockOwner(wpStoreId, parts[1]);
            }
            
            asset = new AssetInfoImpl(version, node, lockOwner);
        }
        
        return asset;
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.asset.AssetService#getLockOwner(org.alfresco.wcm.asset.AssetInfo)
     */
    public String getLockOwner(AssetInfo asset)
    {
        ParameterCheck.mandatory("asset", asset);
        
        return getLockOwner(WCMUtil.getWebProjectStoreId(asset.getSandboxId()), asset.getPath());
    }
    
    private String getLockOwner(String wpStoreId, String filePath)
    {
        String lockOwner = null;
        
        if (avmLockingService != null)
        {
            AVMLock lock = avmLockingService.getLock(wpStoreId, filePath);
    
            if (lock != null)
            {
                // for now assume single lock owner (if more than one, then return first in list)
                List lockUsers = lock.getOwners();
                if (lockUsers.size() > 0)
                {
                    lockOwner = lockUsers.get(0);
                }
            }
        }
        
        return lockOwner;
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.asset.AssetService#hasLockAccess(org.alfresco.wcm.asset.AssetInfo)
     */
    public boolean hasLockAccess(AssetInfo asset)
    {
        ParameterCheck.mandatory("asset", asset);
        
        return avmLockingService.hasAccess(WCMUtil.getWebProjectStoreId(asset.getSandboxId()), asset.getAvmPath(), AuthenticationUtil.getFullyAuthenticatedUser());
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.asset.AssetService#updateAssetProperties(org.alfresco.wcm.asset.AssetInfo, java.util.Map)
     */
    public void updateAssetProperties(AssetInfo asset, Map properties)
    {
        ParameterCheck.mandatory("asset", asset);
        ParameterCheck.mandatory("properties", properties);
        
        NodeRef avmNodeRef = AVMNodeConverter.ToNodeRef(-1, asset.getAvmPath());
        for (Map.Entry prop : properties.entrySet())
        {
            avmNodeService.setProperty(avmNodeRef, prop.getKey(), prop.getValue());
        }
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.asset.AssetService#setAssetProperties(org.alfresco.wcm.asset.AssetInfo, java.util.Map)
     */
    public void setAssetProperties(AssetInfo asset, Map properties)
    {
        ParameterCheck.mandatory("asset", asset);
        ParameterCheck.mandatory("properties", properties);
         
        setProperties(asset.getAvmPath(), properties);
    }
    
    private void setProperties(String avmPath, Map properties)
    {
        NodeRef avmNodeRef = AVMNodeConverter.ToNodeRef(-1, avmPath);
        avmNodeService.setProperties(avmNodeRef, properties);
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.asset.AssetService#addAspect(org.alfresco.wcm.asset.AssetInfo, org.alfresco.service.namespace.QName, java.util.Map)
     */
    public void addAspect(AssetInfo asset, QName aspectName, Map properties)
    {
        addAspect(asset.getAvmPath(), aspectName, properties);
    }
    
    private void addAspect(String avmPath, QName aspect, Map properties)
    {
        NodeRef avmNodeRef = AVMNodeConverter.ToNodeRef(-1, avmPath);
        avmNodeService.addAspect(avmNodeRef, aspect, properties);
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.asset.AssetService#removeAspect(org.alfresco.wcm.asset.AssetInfo, org.alfresco.service.namespace.QName)
     */
    public void removeAspect(AssetInfo asset, QName aspectName)
    {
        ParameterCheck.mandatory("asset", asset);
        
        NodeRef avmNodeRef = AVMNodeConverter.ToNodeRef(-1, asset.getAvmPath());
        avmNodeService.removeAspect(avmNodeRef, aspectName);
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.asset.AssetService#getAspects(org.alfresco.wcm.asset.AssetInfo)
     */
    public Set getAspects(AssetInfo asset)
    {
        ParameterCheck.mandatory("asset", asset);
        
        NodeRef avmNodeRef = AVMNodeConverter.ToNodeRef(asset.getSandboxVersion(), asset.getAvmPath());
        return avmNodeService.getAspects(avmNodeRef);
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.asset.AssetService#hasAspect(org.alfresco.wcm.asset.AssetInfo, org.alfresco.service.namespace.QName)
     */
    public boolean hasAspect(AssetInfo asset, QName aspectName)
    {
        ParameterCheck.mandatory("asset", asset);
        
        NodeRef avmNodeRef = AVMNodeConverter.ToNodeRef(asset.getSandboxVersion(), asset.getAvmPath());
        return avmNodeService.hasAspect(avmNodeRef, aspectName);
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.asset.AssetService#getAssetProperties(org.alfresco.wcm.asset.AssetInfo)
     */
    public Map getAssetProperties(AssetInfo asset)
    {
        ParameterCheck.mandatory("asset", asset);
        
        return getProperties(asset.getSandboxVersion(), asset.getAvmPath());
    }
    
    private Map getProperties(int version, String avmPath)
    {
        NodeRef avmNodeRef = AVMNodeConverter.ToNodeRef(version, avmPath);
        return avmNodeService.getProperties(avmNodeRef); // note: includes built-in properties
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.asset.AssetService#listAssetsWebApp(java.lang.String, java.lang.String, java.lang.String, boolean)
     */
    public List listAssetsWebApp(String sbStoreId, String webApp, String parentFolderPathRelativeToWebApp, boolean includeDeleted)
    {
        ParameterCheck.mandatoryString("sbStoreId", sbStoreId);
        ParameterCheck.mandatoryString("webApp", webApp);
        ParameterCheck.mandatoryString("parentFolderPathRelativeToWebApp", parentFolderPathRelativeToWebApp);
        
        parentFolderPathRelativeToWebApp = addLeadingSlash(parentFolderPathRelativeToWebApp);
        
        String avmPath = WCMUtil.buildStoreWebappPath(sbStoreId, webApp) + parentFolderPathRelativeToWebApp;
        
        return listAssetsAVM(-1, avmPath, includeDeleted);
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.asset.AssetService#listAssets(java.lang.String, java.lang.String, boolean)
     */
    public List listAssets(String sbStoreId, String parentFolderPath, boolean includeDeleted)
    {
        ParameterCheck.mandatoryString("sbStoreId", sbStoreId);
        ParameterCheck.mandatoryString("parentFolderPath", parentFolderPath);
        
        parentFolderPath = addLeadingSlash(parentFolderPath);
        
        String avmPath = sbStoreId + WCMUtil.AVM_STORE_SEPARATOR + parentFolderPath;
        
        return listAssetsAVM(-1, avmPath, includeDeleted);
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.asset.AssetService#listAssets(java.lang.String, int, java.lang.String, boolean)
     */
    public List listAssets(String sbStoreId, int version, String parentFolderPath, boolean includeDeleted)
    {
        ParameterCheck.mandatoryString("sbStoreId", sbStoreId);
        ParameterCheck.mandatoryString("parentFolderPath", parentFolderPath);
        
        parentFolderPath = addLeadingSlash(parentFolderPath);
        
        String avmPath = sbStoreId + WCMUtil.AVM_STORE_SEPARATOR + parentFolderPath;
        
        return listAssetsAVM(version, avmPath, includeDeleted);
    }
    
    private List listAssetsAVM(int version, String avmPath, boolean includeDeleted)
    {
        ParameterCheck.mandatoryString("avmPath", avmPath);
        
        Map nodes = avmService.getDirectoryListing(version, avmPath, includeDeleted);
        
        List assets = new ArrayList(nodes.size());
        for (AVMNodeDescriptor node : nodes.values())
        { 
            String lockOwner = null;
            if (avmLockingService != null)
            {
                String wpStoreId = WCMUtil.getWebProjectStoreIdFromPath(avmPath);
                String[] parts = WCMUtil.splitPath(avmPath);
                lockOwner = getLockOwner(wpStoreId, parts[1]);
            }
            
            assets.add(new AssetInfoImpl(version, node, lockOwner));
        }
        
        return assets;
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.asset.AssetService#deleteAsset(org.alfresco.wcm.asset.AssetInfo)
     */
    public void deleteAsset(AssetInfo asset)
    {
        ParameterCheck.mandatory("asset", asset);
        
        if (! isWebProjectStagingSandbox(asset.getSandboxId()))
        {
            avmService.removeNode(asset.getAvmPath());
        }
        else
        {
            throw new AccessDeniedException("Not allowed to write in: " + asset.getSandboxId());
        }
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.asset.AssetService#renameAsset(org.alfresco.wcm.asset.AssetInfo, java.lang.String)
     */
    public AssetInfo renameAsset(AssetInfo asset, String newName)
    {
        ParameterCheck.mandatory("asset", asset);
        
        if (! isWebProjectStagingSandbox(asset.getSandboxId()))
        {
            String avmParentPath = AVMNodeConverter.SplitBase(asset.getAvmPath())[0];
            String oldName = asset.getName();
            
            avmService.rename(avmParentPath, oldName, avmParentPath, newName);
            
            return getAsset(asset.getSandboxId(), WCMUtil.getStoreRelativePath(avmParentPath)+"/"+newName);
        }
        else
        {
            throw new AccessDeniedException("Not allowed to write in: " + asset.getSandboxId());
        }
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.asset.AssetService#moveAsset(org.alfresco.wcm.asset.AssetInfo, java.lang.String)
     */
    public AssetInfo moveAsset(AssetInfo asset, String parentFolderPath)
    {
        ParameterCheck.mandatory("asset", asset);
        
        if (! isWebProjectStagingSandbox(asset.getSandboxId()))
        {
            parentFolderPath = addLeadingSlash(parentFolderPath);
            
            String avmDstPath = asset.getSandboxId() + WCMUtil.AVM_STORE_SEPARATOR + parentFolderPath;
            
            String avmSrcPath = AVMNodeConverter.SplitBase(asset.getAvmPath())[0];
            String name = asset.getName();
            
            avmService.rename(avmSrcPath, name, avmDstPath, name);
            
            return getAsset(asset.getSandboxId(), WCMUtil.getStoreRelativePath(avmDstPath)+"/"+name);
        }
        else
        {
            throw new AccessDeniedException("Not allowed to write in: " + asset.getSandboxId());
        }
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.wcm.asset.AssetService#copyAsset(org.alfresco.wcm.asset.AssetInfo, java.lang.String)
     */
    public AssetInfo copyAsset(AssetInfo asset, String parentFolderPath)
    {
        ParameterCheck.mandatory("asset", asset);
        
        if (! isWebProjectStagingSandbox(asset.getSandboxId()))
        {
            parentFolderPath = addLeadingSlash(parentFolderPath);
            
            String avmDstParentPath = asset.getSandboxId() + WCMUtil.AVM_STORE_SEPARATOR + parentFolderPath;
            
            String avmSrcPath = asset.getAvmPath();
            String name = asset.getName();
            
            avmService.copy(-1, avmSrcPath, avmDstParentPath, name);
            
            return getAsset(asset.getSandboxId(), WCMUtil.getStoreRelativePath(avmDstParentPath+"/"+name));
        }
        else
        {
            throw new AccessDeniedException("Not allowed to write in: " + asset.getSandboxId());
        }
    }
    
    // TODO should this be in sandbox service ?
    public void bulkImport(String sbStoreId, String parentFolderPath, File zipFile, boolean isHighByteZip)
    {
        if (! isWebProjectStagingSandbox(sbStoreId))
        {
            parentFolderPath = addLeadingSlash(parentFolderPath);
            
            String avmDstPath = sbStoreId + WCMUtil.AVM_STORE_SEPARATOR + parentFolderPath;
        
            // convert the AVM path to a NodeRef so we can use the NodeService to perform import
            NodeRef importRef = AVMNodeConverter.ToNodeRef(-1, avmDstPath);
            processZipImport(zipFile, isHighByteZip, importRef);
        
            // After a bulk import, snapshot the store
            avmService.createSnapshot(sbStoreId, "Import of file: " + zipFile.getName(), null);
            
            // Bind the post-commit transaction listener with data required for virtualization server notification
            UpdateSandboxTransactionListener tl = new UpdateSandboxTransactionListener(avmDstPath);
            AlfrescoTransactionSupport.bindListener(tl);
        }
        else
        {
            throw new AccessDeniedException("Not allowed to write in: " + sbStoreId);
        }
        
    }
    
    /**
     * Process ZIP file for import into an AVM repository store location
     *
     * @param file       ZIP format file
     * @param rootRef    Root reference of the AVM location to import into
     */
    private void processZipImport(File file, boolean isHighByteZip, NodeRef rootRef)
    {
       try
       {
          // NOTE: This encoding allows us to workaround bug:
          //       http://bugs.sun.com/bugdatabase/view_bug.do;:WuuT?bug_id=4820807
          ZipFile zipFile = new ZipFile(file, isHighByteZip ? "Cp437" : null);
          File alfTempDir = TempFileProvider.getTempDir();
          // build a temp dir name based on the name of the file we are importing
          File tempDir = new File(alfTempDir.getPath() + File.separatorChar + file.getName() + "_unpack");
          try
          {
             ImporterActionExecuter.extractFile(zipFile, tempDir.getPath());
             importDirectory(tempDir.getPath(), rootRef);
          }
          finally
          {
             if (tempDir.exists())
             {
                ImporterActionExecuter.deleteDir(tempDir);
             }
          }
       }
       catch (IOException e)
       {
          throw new AlfrescoRuntimeException("Unable to process Zip file. File may not be of the expected format.", e);
       }
    }
    
    /**
     * Recursively import a directory structure into the specified root node
     *
     * @param dir     The directory of files and folders to import
     * @param root    The root node to import into
     */
    private void importDirectory(String dir, NodeRef root)
    {
       File topdir = new File(dir);
       if (!topdir.exists()) return;
       for (File file : topdir.listFiles())
       {
          try
          {
             if (file.isFile())
             {
                // Create a file in the AVM store
                String avmPath = AVMNodeConverter.ToAVMVersionPath(root).getSecond();
                String fileName = file.getName();
                Map titledProps = new HashMap();
                titledProps.put(ContentModel.PROP_TITLE, fileName);
                
                createFileAVM(avmPath, fileName, new BufferedInputStream(new FileInputStream(file), BUFFER_SIZE));
                
                addAspect(avmPath, ContentModel.ASPECT_TITLED, titledProps);
             }
             else
             {
                // Create a directory in the AVM store
                String avmPath = AVMNodeConverter.ToAVMVersionPath(root).getSecond();
                createFolderAVM(avmPath, file.getName(), null);
                String folderPath = avmPath + '/' + file.getName();
                NodeRef folderRef = AVMNodeConverter.ToNodeRef(-1, folderPath);
                importDirectory(file.getPath(), folderRef);
             }
          }
          catch (FileNotFoundException e)
          {
             // TODO: add failed file info to status message?
             throw new AlfrescoRuntimeException("Failed to process ZIP file.", e);
          }
          catch (FileExistsException e)
          {
             // TODO: add failed file info to status message?
             throw new AlfrescoRuntimeException("Failed to process ZIP file.", e);
          }
       }
    }
    
    /**
     * Update Sandbox Transaction listener - invoked after bulk import
     */
    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()
        {
            // Reload virtualisation server as required
            if (this.virtUpdatePath != null)
            {
               WCMUtil.updateVServerWebapp(virtServerRegistry, this.virtUpdatePath, true);
            }
        }
    }
}