/*
 * Copyright (C) 2005-2007 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.filesys.avm;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.StringTokenizer;

import javax.transaction.UserTransaction;

import org.alfresco.config.ConfigElement;
import org.alfresco.filesys.alfresco.AlfrescoDiskDriver;
import org.alfresco.filesys.server.SrvSession;
import org.alfresco.filesys.server.core.DeviceContext;
import org.alfresco.filesys.server.core.DeviceContextException;
import org.alfresco.filesys.server.core.DeviceInterface;
import org.alfresco.filesys.server.filesys.AccessDeniedException;
import org.alfresco.filesys.server.filesys.DirectoryNotEmptyException;
import org.alfresco.filesys.server.filesys.DiskInterface;
import org.alfresco.filesys.server.filesys.FileAttribute;
import org.alfresco.filesys.server.filesys.FileExistsException;
import org.alfresco.filesys.server.filesys.FileInfo;
import org.alfresco.filesys.server.filesys.FileName;
import org.alfresco.filesys.server.filesys.FileOpenParams;
import org.alfresco.filesys.server.filesys.FileStatus;
import org.alfresco.filesys.server.filesys.NetworkFile;
import org.alfresco.filesys.server.filesys.PathNotFoundException;
import org.alfresco.filesys.server.filesys.SearchContext;
import org.alfresco.filesys.server.filesys.TreeConnection;
import org.alfresco.filesys.server.pseudo.PseudoFile;
import org.alfresco.filesys.server.pseudo.PseudoFileList;
import org.alfresco.filesys.server.pseudo.PseudoFolderNetworkFile;
import org.alfresco.filesys.server.state.FileState;
import org.alfresco.filesys.util.StringList;
import org.alfresco.filesys.util.WildCard;
import org.alfresco.repo.avm.CreateStoreTxnListener;
import org.alfresco.repo.avm.CreateVersionTxnListener;
import org.alfresco.repo.avm.PurgeStoreTxnListener;
import org.alfresco.repo.avm.PurgeVersionTxnListener;
import org.alfresco.repo.domain.PropertyValue;
import org.alfresco.repo.security.authentication.AuthenticationComponent;
import org.alfresco.service.cmr.avm.AVMBadArgumentException;
import org.alfresco.service.cmr.avm.AVMExistsException;
import org.alfresco.service.cmr.avm.AVMNodeDescriptor;
import org.alfresco.service.cmr.avm.AVMNotFoundException;
import org.alfresco.service.cmr.avm.AVMService;
import org.alfresco.service.cmr.avm.AVMStoreDescriptor;
import org.alfresco.service.cmr.avm.AVMWrongTypeException;
import org.alfresco.service.cmr.avm.VersionDescriptor;
import org.alfresco.service.cmr.repository.MimetypeService;
import org.alfresco.service.cmr.security.AuthenticationService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.transaction.TransactionService;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * AVM Repository Filesystem Driver Class
 * <p>
 * Provides a filesystem interface for various protocols such as SMB/CIFS and FTP.
 * 
 * @author GKSpencer
 */
public class AVMDiskDriver extends AlfrescoDiskDriver implements DiskInterface
{

    // Logging

    private static final Log logger = LogFactory.getLog(AVMDiskDriver.class);

    // Configuration key names

    private static final String KEY_STORE = "storePath";

    private static final String KEY_VERSION = "version";

    private static final String KEY_CREATE = "createStore";

    // AVM path seperator

    public static final char AVM_SEPERATOR = '/';

    public static final String AVM_SEPERATOR_STR = "/";

    // Services and helpers

    private AVMService m_avmService;

    private TransactionService m_transactionService;

    private MimetypeService m_mimetypeService;

    private AuthenticationComponent m_authComponent;

    private AuthenticationService m_authService;

    // AVM listeners

    private CreateStoreTxnListener m_createStoreListener;

    private PurgeStoreTxnListener m_purgeStoreListener;

    private CreateVersionTxnListener m_createVerListener;

    private PurgeVersionTxnListener m_purgeVerListener;

    /**
     * Default constructor
     */
    public AVMDiskDriver()
    {
    }

    /**
     * Return the AVM service
     * 
     * @return AVMService
     */
    public final AVMService getAvmService()
    {
        return m_avmService;
    }

    /**
     * Return the authentication service
     * 
     * @return AuthenticationService
     */
    public final AuthenticationService getAuthenticationService()
    {
        return m_authService;
    }

    /**
     * Return the transaction service
     * 
     * @return TransactionService
     */
    public final TransactionService getTransactionService()
    {
        return m_transactionService;
    }

    /**
     * Set the AVM service
     * 
     * @param avmService
     *            AVMService
     */
    public void setAvmService(AVMService avmService)
    {
        m_avmService = avmService;
    }

    /**
     * Set the transaction service
     * 
     * @param transactionService
     *            the transaction service
     */
    public void setTransactionService(TransactionService transactionService)
    {
        m_transactionService = transactionService;
    }

    /**
     * Set the authentication component
     * 
     * @param authComponent
     *            AuthenticationComponent
     */
    public void setAuthenticationComponent(AuthenticationComponent authComponent)
    {
        m_authComponent = authComponent;
    }

    /**
     * Set the authentication service
     * 
     * @param authService
     *            AuthenticationService
     */
    public void setAuthenticationService(AuthenticationService authService)
    {
        m_authService = authService;
    }

    /**
     * Set the mimetype service
     * 
     * @param mimetypeService
     *            MimetypeService
     */
    public void setMimetypeService(MimetypeService mimetypeService)
    {
        m_mimetypeService = mimetypeService;
    }

    /**
     * Set the create store listener
     * 
     * @param createStoreListener
     *            CreateStoreTxnListener
     */
    public void setCreateStoreListener(CreateStoreTxnListener createStoreListener)
    {
        m_createStoreListener = createStoreListener;
    }

    /**
     * Set the purge store listener
     * 
     * @param purgeStoreListener
     *            PurgeStoreTxnListener
     */
    public void setPurgeStoreListener(PurgeStoreTxnListener purgeStoreListener)
    {
        m_purgeStoreListener = purgeStoreListener;
    }

    /**
     * Set the create version listener
     * 
     * @param createVersionListener
     *            CreateVersionTxnListener
     */
    public void setCreateVersionListener(CreateVersionTxnListener createVersionListener)
    {
        m_createVerListener = createVersionListener;
    }

    /**
     * Set the purge version listener
     * 
     * @param purgeVersionListener
     *            PurgeVersionTxnListener
     */
    public void setPurgeVersionListener(PurgeVersionTxnListener purgeVersionListener)
    {
        m_purgeVerListener = purgeVersionListener;
    }

    /**
     * Parse and validate the parameter string and create a device context object for this instance of the shared
     * device.
     * 
     * @param devIface
     *            DeviceInterface
     * @param name
     *            String
     * @param cfg
     *            ConfigElement
     * @return DeviceContext
     * @exception DeviceContextException
     */
    public DeviceContext createContext(DeviceInterface devIface, String name, ConfigElement cfg)
            throws DeviceContextException
    {
        // Use the system user as the authenticated context for the filesystem initialization

        String currentUser = m_authComponent.getCurrentUserName();
        try
        {
            m_authComponent.setCurrentUser(m_authComponent.getSystemUserName());

            // Wrap the initialization in a transaction

            UserTransaction tx = m_transactionService.getUserTransaction(false);

            AVMContext context = null;

            try
            {
                // Start the transaction

                if (tx != null)
                    tx.begin();

                // Check if the share is a virtualization view

                ConfigElement virtElem = cfg.getChild("virtualView");
                if (virtElem != null)
                {
                    // Check if sandboxes should be shown in the virtualization view

                    boolean showSandboxes = cfg.getChild("showAllSandboxes") != null ? true : false;

                    // Create the context

                    context = new AVMContext(name, showSandboxes, this);

                    // Enable file state caching

                    context.enableStateTable(true, getStateReaper());

                    // Plug the virtualization view context into the various store/version call back listeners
                    // so that store/version pseudo folders can be kept in sync with AVM

                    m_createStoreListener.addCallback(context);
                    m_purgeStoreListener.addCallback(context);

                    m_createVerListener.addCallback(context);
                    m_purgeVerListener.addCallback(context);
                }
                else
                {
                    // Get the store path

                    ConfigElement storeElement = cfg.getChild(KEY_STORE);
                    if (storeElement == null
                            || storeElement.getValue() == null || storeElement.getValue().length() == 0)
                        throw new DeviceContextException("Device missing init value: " + KEY_STORE);

                    String storePath = storeElement.getValue();

                    // Get the version if specified, or default to the head version

                    int version = AVMContext.VERSION_HEAD;

                    ConfigElement versionElem = cfg.getChild(KEY_VERSION);
                    if (versionElem != null)
                    {
                        // Check if the version is valid

                        if (versionElem.getValue() == null || versionElem.getValue().length() == 0)
                            throw new DeviceContextException("Store version not specified");

                        // Validate the version id

                        try
                        {
                            version = Integer.parseInt(versionElem.getValue());
                        }
                        catch (NumberFormatException ex)
                        {
                            throw new DeviceContextException("Invalid store version specified, "
                                    + versionElem.getValue());
                        }

                        // Range check the version id

                        if (version < 0 && version != AVMContext.VERSION_HEAD)
                            throw new DeviceContextException("Invalid store version id specified, " + version);
                    }

                    // Check if the create flag is enabled

                    ConfigElement createStore = cfg.getChild(KEY_CREATE);

                    // Validate the store path

                    AVMNodeDescriptor rootNode = m_avmService.lookup(version, storePath);
                    if (rootNode == null)
                    {
                        // Check if the store should be created

                        if (createStore == null || version != AVMContext.VERSION_HEAD)
                            throw new DeviceContextException("Invalid store path/version, "
                                    + storePath + " (" + version + ")");

                        // Parse the store path

                        String storeName = null;
                        String path = null;

                        int pos = storePath.indexOf(":/");
                        if (pos != -1)
                        {
                            storeName = storePath.substring(0, pos);
                            if (storePath.length() > pos)
                                path = storePath.substring(pos + 2);
                        }
                        else
                            storeName = storePath;

                        // Check if the store exists

                        AVMStoreDescriptor storeDesc = null;

                        try
                        {
                            storeDesc = m_avmService.getStore(storeName);
                        }
                        catch (AVMNotFoundException ex)
                        {
                        }

                        // Create a new store if it does not exist

                        if (storeDesc == null)
                            m_avmService.createStore(storeName);

                        // Check if there is an optional path

                        if (path != null)
                        {
                            // Split the path

                            StringTokenizer tokens = new StringTokenizer(path, AVMPath.AVM_SEPERATOR_STR);
                            StringList paths = new StringList();

                            while (tokens.hasMoreTokens())
                                paths.addString(tokens.nextToken());

                            // Create the path, or folders that do not exist

                            AVMPath curPath = new AVMPath(storeName, version, FileName.DOS_SEPERATOR_STR);
                            AVMNodeDescriptor curDesc = m_avmService.lookup(curPath.getVersion(), curPath.getAVMPath());

                            // Walk the path checking creating each folder as required

                            for (int i = 0; i < paths.numberOfStrings(); i++)
                            {
                                AVMNodeDescriptor nextDesc = null;

                                try
                                {
                                    // Check if the child folder exists

                                    nextDesc = m_avmService.lookup(curDesc, paths.getStringAt(i));
                                }
                                catch (AVMNotFoundException ex)
                                {
                                }

                                // Check if the folder exists

                                if (nextDesc == null)
                                {
                                    // Create the new folder

                                    m_avmService.createDirectory(curPath.getAVMPath(), paths.getStringAt(i));

                                    // Get the details of the new folder

                                    nextDesc = m_avmService.lookup(curDesc, paths.getStringAt(i));
                                }
                                else if (nextDesc.isFile())
                                    throw new DeviceContextException("Path element error, not a folder, "
                                            + paths.getStringAt(i));

                                // Step to the next level

                                curPath.parsePath(storeName, version, curPath.getRelativePath()
                                        + paths.getStringAt(i) + FileName.DOS_SEPERATOR_STR);
                                curDesc = nextDesc;
                            }
                        }

                        // Validate the store path again

                        rootNode = m_avmService.lookup(version, storePath);
                        if (rootNode == null)
                            throw new DeviceContextException("Failed to create new store " + storePath);
                    }

                    // Create the context

                    context = new AVMContext(name, storePath, version);

                    // Enable file state caching

                    context.enableStateTable(true, getStateReaper());
                }

                // Commit the transaction

                tx.commit();
                tx = null;
            }
            catch (Exception ex)
            {
                logger.error("Error during create context", ex);

                // Rethrow the exception

                throw new DeviceContextException("Driver setup error, " + ex.getMessage());
            }
            finally
            {
                // If there is an active transaction then roll it back

                if (tx != null)
                {
                    try
                    {
                        tx.rollback();
                    }
                    catch (Exception ex)
                    {
                        logger.warn("Failed to rollback transaction", ex);
                    }
                }
            }
            // Return the context for this shared filesystem
            return context;
        }
        finally
        {
            m_authComponent.setCurrentUser(currentUser);
        }

    }

    /**
     * Return a list of the available AVM store names
     * 
     * @return StringList
     */
    public final StringList getAVMStoreNames()
    {
        // Use the system user as the authenticated context to get the AVM store list

        String currentUser = m_authComponent.getCurrentUserName();
        try
        {
            m_authComponent.setCurrentUser(m_authComponent.getSystemUserName());

            // Wrap the service request in a transaction

            UserTransaction tx = m_transactionService.getUserTransaction(false);

            StringList storeNames = new StringList();

            try
            {
                // Start the transaction

                if (tx != null)
                    tx.begin();

                // Get the list of AVM stores

                List<AVMStoreDescriptor> storeList = m_avmService.getStores();

                if (storeList != null)
                {
                    for (AVMStoreDescriptor storeDesc : storeList)
                        storeNames.addString(storeDesc.getName());
                }

                // Commit the transaction

                tx.commit();
                tx = null;
            }
            catch (Exception ex)
            {
                logger.error("Error getting store names", ex);
            }
            finally
            {
                // If there is an active transaction then roll it back

                if (tx != null)
                {
                    try
                    {
                        tx.rollback();
                    }
                    catch (Exception ex)
                    {
                        logger.warn("Failed to rollback transaction", ex);
                    }
                }
            }

            // Return the list of AVM store names

            return storeNames;
        }
        finally
        {
            m_authComponent.setCurrentUser(currentUser);
        }
    }

    /**
     * Get the properties for a store
     * 
     * @param storeName
     *            String
     * @return Map<QName, PropertyValue>
     */
    protected final Map<QName, PropertyValue> getAVMStoreProperties(String storeName)
    {
        // Use the system user as the authenticated context to get the AVM store properties

        String currentUser = m_authComponent.getCurrentUserName();
        try
        {
            m_authComponent.setCurrentUser(m_authComponent.getSystemUserName());

            // Wrap the service request in a transaction

            UserTransaction tx = m_transactionService.getUserTransaction(false);

            Map<QName, PropertyValue> properties = null;

            try
            {
                // Start the transaction

                if (tx != null)
                    tx.begin();

                // Get the list of properties for AVM store

                properties = m_avmService.getStoreProperties(storeName);

                // Commit the transaction

                tx.commit();
                tx = null;
            }
            catch (Exception ex)
            {
                logger.error("Error getting store properties", ex);
            }
            finally
            {
                // If there is an active transaction then roll it back

                if (tx != null)
                {
                    try
                    {
                        tx.rollback();
                    }
                    catch (Exception ex)
                    {
                        logger.warn("Failed to rollback transaction", ex);
                    }
                }
            }

            // Return the list of AVM store properties

            return properties;
        }
        finally
        {
            m_authComponent.setCurrentUser(currentUser);
        }
    }

    /**
     * Build the full store path for a file/folder using the share relative path
     * 
     * @param ctx
     *            AVMContext
     * @param path
     *            String
     * @return AVMPath
     */
    protected final AVMPath buildStorePath(AVMContext ctx, String path)
    {
        // Check if the AVM filesystem is a normal or virtualization view

        AVMPath avmPath = null;

        if (ctx.isVirtualizationView())
        {
            // Create a path for the virtualization view

            avmPath = new AVMPath(path);
        }
        else
        {
            // Create a path to a single store/version

            avmPath = new AVMPath(ctx.getStorePath(), ctx.isVersion(), path);
        }

        // Return the path

        return avmPath;
    }

    /**
     * Close the file.
     * 
     * @param sess
     *            Server session
     * @param tree
     *            Tree connection.
     * @param file
     *            Network file context.
     * @exception java.io.IOException
     *                If an error occurs.
     */
    public void closeFile(SrvSession sess, TreeConnection tree, NetworkFile file) throws java.io.IOException
    {
        // DEBUG

        if (logger.isDebugEnabled())
            logger.debug("Close file " + file.getFullName());

        // Close the file

        file.closeFile();

        // Check if the file/directory is marked for delete

        if (file.hasDeleteOnClose())
        {

            // Check for a file or directory

            if (file.isDirectory())
                deleteDirectory(sess, tree, file.getFullName());
            else
                deleteFile(sess, tree, file.getFullName());
        }
    }

    /**
     * Create a new directory on this file system.
     * 
     * @param sess
     *            Server session
     * @param tree
     *            Tree connection.
     * @param params
     *            Directory create parameters
     * @exception java.io.IOException
     *                If an error occurs.
     */
    public void createDirectory(SrvSession sess, TreeConnection tree, FileOpenParams params) throws java.io.IOException
    {
        // Check if the filesystem is writable

        AVMContext ctx = (AVMContext) tree.getContext();
        if (ctx.isVersion() != AVMContext.VERSION_HEAD)
            throw new AccessDeniedException("Cannot create " + params.getPath() + ", filesys not writable");

        // Split the path to get the new folder name and relative path

        String[] paths = FileName.splitPath(params.getPath());

        // Convert the relative path to a store path

        AVMPath storePath = buildStorePath(ctx, paths[0]);

        // DEBUG

        if (logger.isDebugEnabled())
            logger.debug("Create directory params=" + params + ", storePath=" + storePath + ", name=" + paths[1]);

        // Check if the filesystem is the virtualization view

        if (ctx.isVirtualizationView() && storePath.isReadOnlyPseudoPath())
        {
            throw new AccessDeniedException("Cannot create folder in store/version layer, " + params.getPath());
        }

        // Create a new file

        sess.beginWriteTransaction(m_transactionService);

        try
        {
            // Create the new file entry

            m_avmService.createDirectory(storePath.getAVMPath(), paths[1]);
        }
        catch (AVMExistsException ex)
        {
            throw new FileExistsException(params.getPath());
        }
        catch (AVMNotFoundException ex)
        {
            throw new FileNotFoundException(params.getPath());
        }
        catch (AVMWrongTypeException ex)
        {
            throw new FileNotFoundException(params.getPath());
        }
        catch (AVMBadArgumentException ex)
        {
            throw new FileNotFoundException(params.getPath());
        }
    }

    /**
     * Create a new file on the file system.
     * 
     * @param sess
     *            Server session
     * @param tree
     *            Tree connection
     * @param params
     *            File create parameters
     * @return NetworkFile
     * @exception java.io.IOException
     *                If an error occurs.
     */
    public NetworkFile createFile(SrvSession sess, TreeConnection tree, FileOpenParams params)
            throws java.io.IOException
    {
        // Check if the filesystem is writable

        AVMContext ctx = (AVMContext) tree.getContext();

        // Split the path to get the file name and relative path

        String[] paths = FileName.splitPath(params.getPath());

        // Convert the relative path to a store path

        AVMPath storePath = buildStorePath(ctx, paths[0]);

        // DEBUG

        if (logger.isDebugEnabled())
            logger.debug("Create file params=" + params + ", storePath=" + storePath + ", name=" + paths[1]);

        // Check if the filesystem is the virtualization view

        if (ctx.isVirtualizationView() && storePath.isReadOnlyPseudoPath())
        {
            throw new AccessDeniedException("Cannot create file in store/version layer, " + params.getPath());
        }
        else if (storePath.getVersion() != AVMContext.VERSION_HEAD)
        {
            throw new AccessDeniedException("Cannot create " + params.getPath() + ", filesys not writable");
        }

        // Create a new file

        sess.beginWriteTransaction(m_transactionService);

        AVMNetworkFile netFile = null;

        try
        {
            // Create the new file entry

            m_avmService.createFile(storePath.getAVMPath(), paths[1]).close();

            // Get the new file details

            AVMPath fileStorePath = buildStorePath(ctx, params.getPath());
            AVMNodeDescriptor nodeDesc = m_avmService.lookup(fileStorePath.getVersion(), fileStorePath.getAVMPath());

            if (nodeDesc != null)
            {
                // Create the network file object for the new file

                netFile = new AVMNetworkFile(nodeDesc, fileStorePath.getAVMPath(), fileStorePath.getVersion(),
                        m_avmService);
                netFile.setGrantedAccess(NetworkFile.READWRITE);
                netFile.setFullName(params.getPath());

                netFile.setFileId(fileStorePath.generateFileId());

                // Set the mime-type for the new file

                netFile.setMimeType(m_mimetypeService.guessMimetype(paths[1]));
            }
        }
        catch (AVMExistsException ex)
        {
            throw new FileExistsException(params.getPath());
        }
        catch (AVMNotFoundException ex)
        {
            throw new FileNotFoundException(params.getPath());
        }
        catch (AVMWrongTypeException ex)
        {
            throw new FileNotFoundException(params.getPath());
        }
        catch (AVMBadArgumentException ex)
        {
            throw new FileNotFoundException(params.getPath());
        }

        // Return the file

        return netFile;
    }

    /**
     * Delete the directory from the filesystem.
     * 
     * @param sess
     *            Server session
     * @param tree
     *            Tree connection
     * @param dir
     *            Directory name.
     * @exception java.io.IOException
     *                The exception description.
     */
    public void deleteDirectory(SrvSession sess, TreeConnection tree, String dir) throws java.io.IOException
    {
        // Convert the relative path to a store path

        AVMContext ctx = (AVMContext) tree.getContext();
        AVMPath storePath = buildStorePath(ctx, dir);

        // DEBUG

        if (logger.isDebugEnabled())
            logger.debug("Delete directory, path=" + dir + ", storePath=" + storePath);

        // Check if the filesystem is the virtualization view

        if (ctx.isVirtualizationView() && storePath.isPseudoPath())
        {
            throw new AccessDeniedException("Cannot delete pseudo folder, " + dir);
        }

        // Make sure the path is to a folder before deleting it

        sess.beginWriteTransaction(m_transactionService);

        try
        {
            AVMNodeDescriptor nodeDesc = m_avmService.lookup(storePath.getVersion(), storePath.getAVMPath());
            if (nodeDesc != null)
            {
                // Check that we are deleting a folder

                if (nodeDesc.isDirectory())
                {
                    // Make sure the directory is empty

                    SortedMap<String, AVMNodeDescriptor> fileList = m_avmService.getDirectoryListing(nodeDesc);
                    if (fileList != null && fileList.size() > 0)
                        throw new DirectoryNotEmptyException(dir);

                    // Delete the folder

                    m_avmService.removeNode(storePath.getAVMPath());
                }
                else
                    throw new IOException("Delete directory path is not a directory, " + dir);
            }
        }
        catch (AVMNotFoundException ex)
        {
            throw new IOException("Directory not found, " + dir);
        }
        catch (AVMWrongTypeException ex)
        {
            throw new IOException("Invalid path, " + dir);
        }
    }

    /**
     * Delete the specified file.
     * 
     * @param sess
     *            Server session
     * @param tree
     *            Tree connection
     * @param file
     *            NetworkFile
     * @exception java.io.IOException
     *                The exception description.
     */
    public void deleteFile(SrvSession sess, TreeConnection tree, String name) throws java.io.IOException
    {
        // Convert the relative path to a store path

        AVMContext ctx = (AVMContext) tree.getContext();
        AVMPath storePath = buildStorePath(ctx, name);

        // DEBUG

        if (logger.isDebugEnabled())
            logger.debug("Delete file, path=" + name + ", storePath=" + storePath);

        // Check if the filesystem is the virtualization view

        if (ctx.isVirtualizationView() && storePath.isPseudoPath())
        {
            throw new AccessDeniedException("Cannot delete pseudo file, " + name);
        }

        // Make sure the path is to a file before deleting it

        sess.beginWriteTransaction(m_transactionService);

        try
        {
            AVMNodeDescriptor nodeDesc = m_avmService.lookup(storePath.getVersion(), storePath.getAVMPath());
            if (nodeDesc != null)
            {
                // Check that we are deleting a file

                if (nodeDesc.isFile())
                {
                    // Delete the file

                    m_avmService.removeNode(storePath.getAVMPath());
                }
                else
                    throw new IOException("Delete file path is not a file, " + name);
            }
        }
        catch (AVMNotFoundException ex)
        {
            throw new IOException("File not found, " + name);
        }
        catch (AVMWrongTypeException ex)
        {
            throw new IOException("Invalid path, " + name);
        }
    }

    /**
     * Check if the specified file exists, and whether it is a file or directory.
     * 
     * @param sess
     *            Server session
     * @param tree
     *            Tree connection
     * @param name
     *            java.lang.String
     * @return int
     * @see FileStatus
     */
    public int fileExists(SrvSession sess, TreeConnection tree, String name)
    {
        // Convert the relative path to a store path

        AVMContext ctx = (AVMContext) tree.getContext();
        AVMPath storePath = buildStorePath(ctx, name);

        // DEBUG

        if (logger.isDebugEnabled())
            logger.debug("File exists check, path=" + name + ", storePath=" + storePath);

        // Check if the path is valid

        int status = FileStatus.NotExist;

        if (storePath.isValid() == false)
            return status;

        // Check if the filesystem is the virtualization view

        if (ctx.isVirtualizationView() && storePath.isReadOnlyPseudoPath())
        {
            // Find the file state for the pseudo folder

            FileState fstate = findPseudoState(storePath, ctx);

            if (fstate != null)
            {
                // DEBUG

                if (logger.isDebugEnabled())
                    logger.debug("  Found pseudo file " + fstate);

                // Check if the pseudo file is a file or folder

                if (fstate.isDirectory())
                    status = FileStatus.DirectoryExists;
                else
                    status = FileStatus.FileExists;
            }
            else
            {
                // Invalid pseudo file path

                status = FileStatus.NotExist;
            }

            // Return the file status

            return status;
        }

        // Search for the file/folder

        sess.beginReadTransaction(m_transactionService);

        AVMNodeDescriptor nodeDesc = m_avmService.lookup(storePath.getVersion(), storePath.getAVMPath());

        if (nodeDesc != null)
        {
            // Check if the path is to a file or folder

            if (nodeDesc.isDirectory())
                status = FileStatus.DirectoryExists;
            else
                status = FileStatus.FileExists;
        }

        // Return the file status

        return status;
    }

    /**
     * Flush any buffered output for the specified file.
     * 
     * @param sess
     *            Server session
     * @param tree
     *            Tree connection
     * @param file
     *            Network file context.
     * @exception java.io.IOException
     *                The exception description.
     */
    public void flushFile(SrvSession sess, TreeConnection tree, NetworkFile file) throws java.io.IOException
    {
        // Flush the file

        file.flushFile();
    }

    /**
     * Get the file information for the specified file.
     * 
     * @param sess
     *            Server session
     * @param tree
     *            Tree connection
     * @param name
     *            File name/path that information is required for.
     * @return File information if valid, else null
     * @exception java.io.IOException
     *                The exception description.
     */
    public FileInfo getFileInformation(SrvSession sess, TreeConnection tree, String name) throws java.io.IOException
    {
        // Convert the relative path to a store path

        AVMContext ctx = (AVMContext) tree.getContext();
        AVMPath storePath = buildStorePath(ctx, name);

        // DEBUG

        if (logger.isDebugEnabled())
            logger.debug("Get file information, path=" + name + ", storePath=" + storePath);

        // Check if hte path is valid

        if (storePath.isValid() == false)
            throw new FileNotFoundException(name);

        // Check if the filesystem is the virtualization view

        if (ctx.isVirtualizationView() && storePath.isReadOnlyPseudoPath())
        {
            // Check if the search path is for the root, a store or version folder

            if (storePath.isRootPath())
            {
                // Return dummy file informatiom for the root folder

                return new FileInfo(name, 0L, FileAttribute.Directory);
            }
            else
            {
                // Find the pseudo file for the store/version folder

                PseudoFile psFile = findPseudoFolder(storePath, ctx);
                if (psFile != null)
                {
                    // DEBUG

                    if (logger.isDebugEnabled())
                        logger.debug("  Found pseudo file " + psFile);
                    return psFile.getFileInfo();
                }
                else
                    throw new FileNotFoundException(name);
            }
        }

        // Search for the file/folder

        sess.beginReadTransaction(m_transactionService);

        FileInfo info = null;

        try
        {
            AVMNodeDescriptor nodeDesc = m_avmService.lookup(storePath.getVersion(), storePath.getAVMPath());

            if (nodeDesc != null)
            {
                // Create, and fill in, the file information

                info = new FileInfo();

                info.setFileName(nodeDesc.getName());

                if (nodeDesc.isFile())
                {
                    info.setFileSize(nodeDesc.getLength());
                    info.setAllocationSize((nodeDesc.getLength() + 512L) & 0xFFFFFFFFFFFFFE00L);
                }
                else
                    info.setFileSize(0L);

                info.setAccessDateTime(nodeDesc.getAccessDate());
                info.setCreationDateTime(nodeDesc.getCreateDate());
                info.setModifyDateTime(nodeDesc.getModDate());

                // Build the file attributes

                int attr = 0;

                if (nodeDesc.isDirectory())
                    attr += FileAttribute.Directory;

                if (nodeDesc.getName().startsWith(".")
                        || nodeDesc.getName().equalsIgnoreCase("Desktop.ini")
                        || nodeDesc.getName().equalsIgnoreCase("Thumbs.db"))
                    attr += FileAttribute.Hidden;

                // Mark the file/folder as read-only if not the head version

                if (ctx.isVersion() != AVMContext.VERSION_HEAD)
                    attr += FileAttribute.ReadOnly;

                info.setFileAttributes(attr);

                // Set the file id

                info.setFileId(storePath.generateFileId());

                // DEBUG

                if (logger.isDebugEnabled())
                    logger.debug("  File info=" + info);
            }
        }
        catch (AVMNotFoundException ex)
        {
            throw new FileNotFoundException(name);
        }
        catch (AVMWrongTypeException ex)
        {
            throw new PathNotFoundException(name);
        }

        // Return the file information

        return info;
    }

    /**
     * Determine if the disk device is read-only.
     * 
     * @param sess
     *            Server session
     * @param ctx
     *            Device context
     * @return boolean
     * @exception java.io.IOException
     *                If an error occurs.
     */
    public boolean isReadOnly(SrvSession sess, DeviceContext ctx) throws java.io.IOException
    {
        // Check if the version indicates the head version, only the head is writable

        AVMContext avmCtx = (AVMContext) ctx;
        return avmCtx.isVersion() == AVMContext.VERSION_HEAD ? true : false;
    }

    /**
     * Open a file on the file system.
     * 
     * @param sess
     *            Server session
     * @param tree
     *            Tree connection
     * @param params
     *            File open parameters
     * @return NetworkFile
     * @exception java.io.IOException
     *                If an error occurs.
     */
    public NetworkFile openFile(SrvSession sess, TreeConnection tree, FileOpenParams params) throws java.io.IOException
    {
        // Convert the relative path to a store path

        AVMContext ctx = (AVMContext) tree.getContext();
        AVMPath storePath = buildStorePath(ctx, params.getPath());

        // DEBUG

        if (logger.isDebugEnabled())
            logger.debug("Open file params=" + params + ", storePath=" + storePath);

        // Check if the filesystem is the virtualization view

        if (ctx.isVirtualizationView() && storePath.isReadOnlyPseudoPath())
        {
            // Check if the path is for the root, a store or version folder

            if (storePath.isRootPath())
            {
                // Return a dummy file for the root folder

                return new PseudoFolderNetworkFile(FileName.DOS_SEPERATOR_STR);
            }
            else
            {
                // Find the pseudo file for the store/version folder

                PseudoFile psFile = findPseudoFolder(storePath, ctx);
                if (psFile != null)
                {
                    // DEBUG

                    if (logger.isDebugEnabled())
                        logger.debug("  Found pseudo file " + psFile);
                    return psFile.getFile(params.getPath());
                }
                else
                    return null;
            }
        }

        // Search for the file/folder

        sess.beginReadTransaction(m_transactionService);

        AVMNetworkFile netFile = null;

        try
        {
            // Get the details of the file/folder

            AVMNodeDescriptor nodeDesc = m_avmService.lookup(storePath.getVersion(), storePath.getAVMPath());

            if (nodeDesc != null)
            {
                // Check if the filesystem is read-only and write access has been requested

                if (storePath.getVersion() != AVMContext.VERSION_HEAD
                        && (params.isReadWriteAccess() || params.isWriteOnlyAccess()))
                    throw new AccessDeniedException("File " + params.getPath() + " is read-only");

                // Create the network file object for the opened file/folder

                netFile = new AVMNetworkFile(nodeDesc, storePath.getAVMPath(), storePath.getVersion(), m_avmService);

                if (params.isReadOnlyAccess() || storePath.getVersion() != AVMContext.VERSION_HEAD)
                    netFile.setGrantedAccess(NetworkFile.READONLY);
                else
                    netFile.setGrantedAccess(NetworkFile.READWRITE);

                netFile.setFullName(params.getPath());
                netFile.setFileId(storePath.generateFileId());

                // Set the mime-type for the new file

                netFile.setMimeType(m_mimetypeService.guessMimetype(params.getPath()));
            }
            else
                throw new FileNotFoundException(params.getPath());
        }
        catch (AVMNotFoundException ex)
        {
            throw new FileNotFoundException(params.getPath());
        }
        catch (AVMWrongTypeException ex)
        {
            throw new FileNotFoundException(params.getPath());
        }

        // Return the file

        return netFile;
    }

    /**
     * Read a block of data from the specified file.
     * 
     * @param sess
     *            Session details
     * @param tree
     *            Tree connection
     * @param file
     *            Network file
     * @param buf
     *            Buffer to return data to
     * @param bufPos
     *            Starting position in the return buffer
     * @param siz
     *            Maximum size of data to return
     * @param filePos
     *            File offset to read data
     * @return Number of bytes read
     * @exception java.io.IOException
     *                The exception description.
     */
    public int readFile(SrvSession sess, TreeConnection tree, NetworkFile file, byte[] buf, int bufPos, int siz,
            long filePos) throws java.io.IOException
    {
        // Check if the file is a directory

        if (file.isDirectory())
            throw new AccessDeniedException();

        // If the content channel is not open for the file then start a transaction

        AVMNetworkFile avmFile = (AVMNetworkFile) file;

        if (avmFile.hasContentChannel() == false)
            sess.beginReadTransaction(m_transactionService);

        // Read the file

        int rdlen = file.readFile(buf, siz, bufPos, filePos);

        // If we have reached end of file return a zero length read

        if (rdlen == -1)
            rdlen = 0;

        // Return the actual read length

        return rdlen;
    }

    /**
     * Rename the specified file.
     * 
     * @param sess
     *            Server session
     * @param tree
     *            Tree connection
     * @param oldName
     *            java.lang.String
     * @param newName
     *            java.lang.String
     * @exception java.io.IOException
     *                The exception description.
     */
    public void renameFile(SrvSession sess, TreeConnection tree, String oldName, String newName)
            throws java.io.IOException
    {
        // Split the relative paths into parent and file/folder name pairs

        AVMContext ctx = (AVMContext) tree.getContext();

        String[] oldPaths = FileName.splitPath(oldName);
        String[] newPaths = FileName.splitPath(newName);

        // Convert the parent paths to store paths

        AVMPath oldAVMPath = buildStorePath(ctx, oldPaths[0]);
        AVMPath newAVMPath = buildStorePath(ctx, newPaths[0]);

        // DEBUG

        if (logger.isDebugEnabled())
        {
            logger.debug("Rename from path=" + oldPaths[0] + ", name=" + oldPaths[1]);
            logger.debug("        new path=" + newPaths[0] + ", name=" + newPaths[1]);
        }

        // Check if the filesystem is the virtualization view

        if (ctx.isVirtualizationView() && oldAVMPath.isReadOnlyPseudoPath())
        {
            throw new AccessDeniedException("Cannot rename folder in store/version layer, " + oldName);
        }

        // Start a transaction for the rename

        sess.beginWriteTransaction(m_transactionService);

        try
        {
            // Rename the file/folder

            m_avmService.rename(oldAVMPath.getAVMPath(), oldPaths[1], newAVMPath.getAVMPath(), newPaths[1]);
        }
        catch (AVMNotFoundException ex)
        {
            throw new IOException("Source not found, " + oldName);
        }
        catch (AVMWrongTypeException ex)
        {
            throw new IOException("Invalid path, " + oldName);
        }
        catch (AVMExistsException ex)
        {
            throw new FileExistsException("Destination exists, " + newName);
        }
    }

    /**
     * Seek to the specified file position.
     * 
     * @param sess
     *            Server session
     * @param tree
     *            Tree connection
     * @param file
     *            Network file.
     * @param pos
     *            Position to seek to.
     * @param typ
     *            Seek type.
     * @return New file position, relative to the start of file.
     */
    public long seekFile(SrvSession sess, TreeConnection tree, NetworkFile file, long pos, int typ)
            throws java.io.IOException
    {
        // Check if the file is a directory

        if (file.isDirectory())
            throw new AccessDeniedException();

        // If the content channel is not open for the file then start a transaction

        AVMNetworkFile avmFile = (AVMNetworkFile) file;

        if (avmFile.hasContentChannel() == false)
            sess.beginReadTransaction(m_transactionService);

        // Set the file position

        return file.seekFile(pos, typ);
    }

    /**
     * Set the file information for the specified file.
     * 
     * @param sess
     *            Server session
     * @param tree
     *            Tree connection
     * @param name
     *            java.lang.String
     * @param info
     *            FileInfo
     * @exception java.io.IOException
     *                The exception description.
     */
    public void setFileInformation(SrvSession sess, TreeConnection tree, String name, FileInfo info)
            throws java.io.IOException
    {
        // Check if the file is being marked for deletion, check if the file is writable

        if (info.hasSetFlag(FileInfo.SetDeleteOnClose) && info.hasDeleteOnClose())
        {
            // If this is not the head version then it's not writable

            AVMContext avmCtx = (AVMContext) tree.getContext();
            if (avmCtx.isVersion() != AVMContext.VERSION_HEAD)
                throw new AccessDeniedException("Store not writable, cannot set delete on close");
        }
    }

    /**
     * Start a new search on the filesystem using the specified searchPath that may contain wildcards.
     * 
     * @param sess
     *            Server session
     * @param tree
     *            Tree connection
     * @param searchPath
     *            File(s) to search for, may include wildcards.
     * @param attrib
     *            Attributes of the file(s) to search for, see class SMBFileAttribute.
     * @return SearchContext
     * @exception java.io.FileNotFoundException
     *                If the search could not be started.
     */
    public SearchContext startSearch(SrvSession sess, TreeConnection tree, String searchPath, int attrib)
            throws java.io.FileNotFoundException
    {
        // Access the AVM context

        AVMContext avmCtx = (AVMContext) tree.getContext();

        // DEBUG

        if (logger.isDebugEnabled())
            logger.debug("Start search path=" + searchPath);

        // Split the search path into relative path and search name

        String[] paths = FileName.splitPath(searchPath);

        // Build the store path to the folder being searched

        AVMPath storePath = buildStorePath(avmCtx, paths[0]);

        // Check if the filesystem is the virtualization view

        if (avmCtx.isVirtualizationView())
        {
            // Check for a search of a pseudo folder

            if (storePath.isReadOnlyPseudoPath())
            {
                // Get the file state for the folder being searched

                FileState fstate = findPseudoState(storePath, avmCtx);

                if (fstate != null)
                {
                    // Get the pseudo file list for the parent directory

                    PseudoFileList searchList = fstate.getPseudoFileList();

                    // Check if this is a single file or wildcard search

                    if (WildCard.containsWildcards(searchPath))
                    {
                        // Check if the pseudo file list is valid

                        if (searchList == null)
                            searchList = new PseudoFileList();

                        // Create the search context, wildcard filter will take care of secondary filtering of the
                        // folder listing

                        WildCard wildCardFilter = new WildCard(paths[1], false);
                        return new PseudoFileListSearchContext(searchList, attrib, wildCardFilter);
                    }
                    else
                    {
                        // Search the pseudo file list for the required file

                        PseudoFile pseudoFile = searchList.findFile(paths[1], false);
                        if (pseudoFile != null)
                        {
                            // Create a search context using the single file details

                            PseudoFileList singleList = new PseudoFileList();
                            singleList.addFile(pseudoFile);

                            return new PseudoFileListSearchContext(singleList, attrib, null);
                        }
                    }
                }

                // File not found

                throw new FileNotFoundException(searchPath);
            }
            else if (storePath.isLevel() == AVMPath.LevelId.HeadMetaData
                    || storePath.isLevel() == AVMPath.LevelId.VersionMetaData)
            {
                // Return an empty file list for now

                PseudoFileList metaFiles = new PseudoFileList();

                return new PseudoFileListSearchContext(metaFiles, attrib, null);
            }
        }

        // Check if the path is a wildcard search

        sess.beginReadTransaction(m_transactionService);
        SearchContext context = null;

        if (WildCard.containsWildcards(searchPath))
        {
            // Get the file listing for the folder

            AVMNodeDescriptor[] fileList = m_avmService.getDirectoryListingArray(storePath.getVersion(), storePath
                    .getAVMPath(), false);

            // Create the search context

            if (fileList != null)
            {

                // DEBUG

                if (logger.isDebugEnabled())
                    logger.debug("  Wildcard search returned " + fileList.length + " files");

                // Create the search context, wildcard filter will take care of secondary filtering of the
                // folder listing

                WildCard wildCardFilter = new WildCard(paths[1], false);
                context = new AVMSearchContext(fileList, attrib, wildCardFilter, storePath.getRelativePath());
            }
        }
        else
        {
            // Single file/folder search, convert the path to a store path

            storePath = buildStorePath(avmCtx, searchPath);

            // Get the single file/folder details

            AVMNodeDescriptor nodeDesc = m_avmService.lookup(storePath.getVersion(), storePath.getAVMPath());

            if (nodeDesc != null)
            {
                // Create the search context for the single file/folder

                context = new AVMSingleFileSearchContext(nodeDesc, storePath.getRelativePath());
            }

        }

        // Return the search context

        return context;
    }

    /**
     * Truncate a file to the specified size
     * 
     * @param sess
     *            Server session
     * @param tree
     *            Tree connection
     * @param file
     *            Network file details
     * @param siz
     *            New file length
     * @exception java.io.IOException
     *                The exception description.
     */
    public void truncateFile(SrvSession sess, TreeConnection tree, NetworkFile file, long siz)
            throws java.io.IOException
    {
        // Check if the file is a directory, or only has read access

        if (file.getGrantedAccess() == NetworkFile.READONLY)
            throw new AccessDeniedException();

        // If the content channel is not open for the file then start a transaction

        AVMNetworkFile avmFile = (AVMNetworkFile) file;

        if (avmFile.hasContentChannel() == false || avmFile.isWritable() == false)
            sess.beginWriteTransaction(m_transactionService);

        // Truncate or extend the file

        file.truncateFile(siz);
        file.flushFile();
    }

    /**
     * Write a block of data to the file.
     * 
     * @param sess
     *            Server session
     * @param tree
     *            Tree connection
     * @param file
     *            Network file details
     * @param buf
     *            byte[] Data to be written
     * @param bufoff
     *            Offset within the buffer that the data starts
     * @param siz
     *            int Data length
     * @param fileoff
     *            Position within the file that the data is to be written.
     * @return Number of bytes actually written
     * @exception java.io.IOException
     *                The exception description.
     */
    public int writeFile(SrvSession sess, TreeConnection tree, NetworkFile file, byte[] buf, int bufoff, int siz,
            long fileoff) throws java.io.IOException
    {
        // Check if the file is a directory, or only has read access

        if (file.isDirectory() || file.getGrantedAccess() == NetworkFile.READONLY)
            throw new AccessDeniedException();

        // If the content channel is not open for the file, or the channel is not writable, then start a transaction

        AVMNetworkFile avmFile = (AVMNetworkFile) file;

        if (avmFile.hasContentChannel() == false || avmFile.isWritable() == false)
            sess.beginWriteTransaction(m_transactionService);

        // Write the data to the file

        file.writeFile(buf, siz, bufoff, fileoff);

        // Return the actual write length

        return siz;
    }

    /**
     * Connection opened to this disk device
     * 
     * @param sess
     *            Server session
     * @param tree
     *            Tree connection
     */
    public void treeClosed(SrvSession sess, TreeConnection tree)
    {
        // Nothing to do
    }

    /**
     * Connection closed to this device
     * 
     * @param sess
     *            Server session
     * @param tree
     *            Tree connection
     */
    public void treeOpened(SrvSession sess, TreeConnection tree)
    {
        // Nothing to do
    }

    /**
     * Find the pseudo file for a virtual path
     * 
     * @param avmPath
     *            AVMPath
     * @param avmCtx
     *            AVMContext
     * @return PseudoFile
     */
    private final PseudoFile findPseudoFolder(AVMPath avmPath, AVMContext avmCtx)
    {
        return findPseudoFolder(avmPath, avmCtx, true);
    }

    /**
     * Find the pseudo file for a virtual path
     * 
     * @param avmPath
     *            AVMPath
     * @param avmCtx
     *            AVMContext
     * @param generateStates
     *            boolean
     * @return PseudoFile
     */
    private final PseudoFile findPseudoFolder(AVMPath avmPath, AVMContext avmCtx, boolean generateStates)
    {
        // Check if the path is to a store pseudo folder

        if (avmPath.isRootPath())
            return null;

        // Get the file state for the parent of the required folder

        FileState fstate = null;
        StringBuilder str = null;
        PseudoFile psFile = null;

        switch (avmPath.isLevel())
        {
        // Store root folder

        case StoreRoot:

            // Get the root folder file state

            fstate = avmCtx.getStateTable().findFileState(FileName.DOS_SEPERATOR_STR);

            if (fstate != null && fstate.hasPseudoFiles())
                psFile = fstate.getPseudoFileList().findFile(avmPath.getStoreName(), false);
            break;

        // Versions root or Head folder

        case VersionRoot:
        case Head:

            // Create a path to the parent store

            str = new StringBuilder();

            str.append(FileName.DOS_SEPERATOR);
            str.append(avmPath.getStoreName());

            // Find/create the file state for the store

            AVMPath storePath = new AVMPath(str.toString());
            fstate = findPseudoState(storePath, avmCtx);

            // Find the version root or head pseudo folder

            if (fstate != null)
            {
                if (avmPath.isLevel() == AVMPath.LevelId.Head)
                    psFile = fstate.getPseudoFileList().findFile(AVMPath.VersionNameHead, true);
                else
                    psFile = fstate.getPseudoFileList().findFile(AVMPath.VersionsFolder, true);
            }
            break;

        // Version folder

        case Version:

            // Create a path to the versions folder

            str = new StringBuilder();

            str.append(FileName.DOS_SEPERATOR);
            str.append(avmPath.getStoreName());
            str.append(FileName.DOS_SEPERATOR);
            str.append(AVMPath.VersionsFolder);

            // Find/create the file state for the store

            AVMPath verrootPath = new AVMPath(str.toString());
            fstate = findPseudoState(verrootPath, avmCtx);

            // Find the version pseudo file

            if (fstate != null)
            {
                // Build the version folder name string

                str.setLength(0);

                str.append(AVMPath.VersionFolderPrefix);
                str.append(avmPath.getVersion());

                // find the version folder pseduo file

                psFile = fstate.getPseudoFileList().findFile(str.toString(), true);
            }
            break;

        // Head data or metadata folder

        case HeadData:
        case HeadMetaData:

            // Create a path to the head folder

            str = new StringBuilder();

            str.append(FileName.DOS_SEPERATOR);
            str.append(avmPath.getStoreName());
            str.append(FileName.DOS_SEPERATOR);
            str.append(AVMPath.VersionNameHead);

            // Find/create the file state for the store

            AVMPath headPath = new AVMPath(str.toString());
            fstate = findPseudoState(headPath, avmCtx);

            // Find the data or metadata pseudo folder

            if (fstate != null)
            {
                // Find the pseudo folder

                if (avmPath.isLevel() == AVMPath.LevelId.HeadData)
                {
                    psFile = fstate.getPseudoFileList().findFile(AVMPath.DataFolder, true);
                }
                else
                {
                    psFile = fstate.getPseudoFileList().findFile(AVMPath.MetaDataFolder, true);
                }
            }
            break;

        // Version data or metadata folder

        case VersionData:
        case VersionMetaData:

            // Create a path to the version folder

            str = new StringBuilder();

            str.append(FileName.DOS_SEPERATOR);
            str.append(avmPath.getStoreName());
            str.append(FileName.DOS_SEPERATOR);
            str.append(AVMPath.VersionFolderPrefix);
            str.append(avmPath.getVersion());

            // Find/create the file state for the store

            AVMPath verPath = new AVMPath(str.toString());
            fstate = findPseudoState(verPath, avmCtx);

            // Find the data or metadata pseudo folder

            if (fstate != null)
            {
                // Find the pseudo folder

                if (avmPath.isLevel() == AVMPath.LevelId.VersionData)
                {
                    psFile = fstate.getPseudoFileList().findFile(AVMPath.DataFolder, true);
                }
                else
                {
                    psFile = fstate.getPseudoFileList().findFile(AVMPath.MetaDataFolder, true);
                }
            }
            break;
        }

        // Check if the pseudo file was not found but file states should be generated

        if (psFile == null && generateStates == true)
        {
            // Generate the file states for the path, this is required if a request is made to a path without
            // walking the folder tree

            generatePseudoFolders(avmPath, avmCtx);

            // Try and find the pseudo file again

            psFile = findPseudoFolder(avmPath, avmCtx, false);
        }

        // Return the pseudo file, or null if not found

        return psFile;
    }

    /**
     * Find the file state for a pseudo folder path
     * 
     * @param avmPath
     *            AVMPath
     * @param avmCtx
     *            AVMContext
     * @return FileState
     */
    private final FileState findPseudoState(AVMPath avmPath, AVMContext avmCtx)
    {
        // Make sure the is to a pseudo file/folder

        if (avmPath.isPseudoPath() == false)
            return null;

        // Check if the path is to a store pseudo folder

        FileState fstate = null;
        StringBuilder str = null;

        switch (avmPath.isLevel())
        {
        // Root of the hieararchy

        case Root:

            // Get the root path file state

            fstate = avmCtx.getStateTable().findFileState(FileName.DOS_SEPERATOR_STR);

            // Check if the root file state is valid

            if (fstate == null)
            {
                // Create a file state for the root folder

                fstate = avmCtx.getStateTable().findFileState(FileName.DOS_SEPERATOR_STR, true, true);
                fstate.setExpiryTime(FileState.NoTimeout);

                // Get a list of the available AVM stores

                List<AVMStoreDescriptor> storeList = m_avmService.getStores();

                if (storeList != null && storeList.size() > 0)
                {
                    // Add pseudo files for the stores

                    boolean sandbox = false;

                    for (AVMStoreDescriptor storeDesc : storeList)
                    {
                        // Get the properties for the current store

                        Map<QName, PropertyValue> props = m_avmService.getStoreProperties(storeDesc.getName());

                        if (props.containsKey(AVMContext.PROP_WORKFLOWPREVIEW)
                                || props.containsKey(AVMContext.PROP_AUTHORPREVIEW))
                            sandbox = true;
                        else
                            sandbox = false;

                        // DEBUG

                        if (logger.isDebugEnabled())
                            logger.debug("Store " + storeDesc.getName() + ", sandbox=" + sandbox);

                        // Add a pseudo file for the current store

                        if (sandbox == false || avmCtx.showSandboxes() == true)
                            fstate.addPseudoFile(new StorePseudoFile(storeDesc));
                    }
                }
            }
            break;

        // Store folder

        case StoreRoot:

            // Build the path to the parent store folder

            str = new StringBuilder();

            str.append(FileName.DOS_SEPERATOR);
            str.append(avmPath.getStoreName());

            // Search for the file state for the store pseudo folder

            fstate = avmCtx.getStateTable().findFileState(str.toString());

            if (fstate == null)
            {
                // Create a file state for the store path

                fstate = avmCtx.getStateTable().findFileState(str.toString(), true, true);

                // Add a pseudo file for the head version

                fstate.addPseudoFile(new VersionPseudoFile(AVMPath.VersionNameHead));

                // Add a pseudo file for the version root folder

                fstate.addPseudoFile(new DummyFolderPseudoFile(AVMPath.VersionsFolder));
            }
            break;

        // Head folder

        case Head:

            // Build the path to the store head version folder

            str = new StringBuilder();

            str.append(FileName.DOS_SEPERATOR);
            str.append(avmPath.getStoreName());
            str.append(FileName.DOS_SEPERATOR);
            str.append(AVMPath.VersionNameHead);

            // Search for the file state for the store head version pseudo folder

            fstate = avmCtx.getStateTable().findFileState(str.toString());

            if (fstate == null)
            {
                // Create a file state for the store head folder path

                fstate = avmCtx.getStateTable().findFileState(str.toString(), true, true);

                // Add a pseudo file for the data pseudo folder

                fstate.addPseudoFile(new DummyFolderPseudoFile(AVMPath.DataFolder));

                // Add a pseudo file for the metadata pseudo folder

                fstate.addPseudoFile(new DummyFolderPseudoFile(AVMPath.MetaDataFolder));
            }
            break;

        // Version root folder

        case VersionRoot:

            // Get the list of AVM store versions

            try
            {
                // Build the path to the parent store folder

                str = new StringBuilder();

                str.append(FileName.DOS_SEPERATOR);
                str.append(avmPath.getStoreName());
                str.append(FileName.DOS_SEPERATOR);
                str.append(AVMPath.VersionsFolder);

                // Create a file state for the store path

                fstate = avmCtx.getStateTable().findFileState(str.toString(), true, true);

                // Add pseudo folders if the list is empty

                if (fstate.hasPseudoFiles() == false)
                {
                    // Build the version folder name for the head version

                    StringBuilder verStr = new StringBuilder(AVMPath.VersionFolderPrefix);
                    verStr.append("-1");

                    // Add a pseudo file for the head version

                    fstate.addPseudoFile(new VersionPseudoFile(verStr.toString()));

                    // Get the list of versions for the store

                    List<VersionDescriptor> verList = m_avmService.getStoreVersions(avmPath.getStoreName());

                    // Add pseudo files for the versions to the store state

                    if (verList.size() > 0)
                    {
                        for (VersionDescriptor verDesc : verList)
                        {
                            // Generate the version string

                            String verName = null;

                            verStr.setLength(AVMPath.VersionFolderPrefix.length());
                            verStr.append(verDesc.getVersionID());

                            verName = verStr.toString();

                            // Add the version pseudo folder

                            fstate.addPseudoFile(new VersionPseudoFile(verName, verDesc));
                        }
                    }
                }
            }
            catch (AVMNotFoundException ex)
            {
                // Invalid store name
            }
            break;

        // Version folder

        case Version:

            // Build the path to the store version folder

            str = new StringBuilder();

            str.append(FileName.DOS_SEPERATOR);
            str.append(avmPath.getStoreName());
            str.append(FileName.DOS_SEPERATOR);
            str.append(AVMPath.VersionFolderPrefix);
            str.append(avmPath.getVersion());

            // Search for the file state for the version pseudo folder

            fstate = avmCtx.getStateTable().findFileState(str.toString());

            if (fstate == null)
            {
                // Create a file state for the version folder path

                fstate = avmCtx.getStateTable().findFileState(str.toString(), true, true);

                // Add a pseudo file for the data pseudo folder

                fstate.addPseudoFile(new DummyFolderPseudoFile(AVMPath.DataFolder));

                // Add a pseudo file for the metadata pseudo folder

                fstate.addPseudoFile(new DummyFolderPseudoFile(AVMPath.MetaDataFolder));
            }
            break;
        }

        // Return the file state

        return fstate;
    }

    /**
     * Generate the pseudo folders for the specified path
     * 
     * @param avmPath
     *            AVMPath
     * @param avmCtx
     *            AVMContext
     */
    private final void generatePseudoFolders(AVMPath avmPath, AVMContext avmCtx)
    {
        // Create the root file state

        AVMPath createPath = new AVMPath();
        StringBuilder pathStr = new StringBuilder();

        pathStr.append(FileName.DOS_SEPERATOR);
        createPath.parsePath(pathStr.toString());

        FileState rootState = findPseudoState(createPath, avmCtx);

        // Check if the path has a store name

        if (avmPath.getStoreName() != null)
        {
            // Check if the store name is valid

            if (rootState.hasPseudoFiles()
                    && rootState.getPseudoFileList().findFile(avmPath.getStoreName(), false) != null)
            {
                // Create the store file state

                pathStr.append(avmPath.getStoreName());
                pathStr.append(FileName.DOS_SEPERATOR);

                createPath.parsePath(pathStr.toString());

                findPseudoState(createPath, avmCtx);

                // Add the head and version root pseudo folders

                createPath.parsePath(pathStr.toString() + AVMPath.VersionNameHead);
                findPseudoState(createPath, avmCtx);

                createPath.parsePath(pathStr.toString() + AVMPath.VersionsFolder);
                findPseudoState(createPath, avmCtx);

                // Check if the path is to a version folder

                if (avmPath.isLevel().ordinal() >= AVMPath.LevelId.Version.ordinal())
                {
                    // Build the path

                    pathStr.append(AVMPath.VersionsFolder);
                    pathStr.append(FileName.DOS_SEPERATOR);
                    pathStr.append(AVMPath.VersionFolderPrefix);
                    pathStr.append(avmPath.getVersion());

                    createPath.parsePath(pathStr.toString());

                    // Generate the version folders

                    findPseudoState(createPath, avmCtx);
                }
            }
        }
    }
}