
// Copyright (c) 2000 Oracle Corporation
package oracle.portal.provider.v1;

import java.io.*;
import java.util.*;
import javax.servlet.*;

import oracle.portal.provider.v1.*;
import oracle.portal.provider.v1.ProviderXtra2;
import oracle.portal.utils.v1.MultiByteEncoder;
import oracle.webdb.utils.SimpleStringBuffer;

/**
 * The FilePersonalizationManager implements a PortletPersonalizationManager
 * using data objects to persist customized data to/from the filesystem. The
 * class of the data objects managed by a particular instance of the
 * FilePersonalizationManager is registered using {@link
 * #setDataClass(String)}.  Most commonly this will be specified in the
 * provider.xml file
 * (e.g. <code>&lt;dataClass><i>className</i>&lt;/dataClass></code>). If your
 * data class implements the {@link
 * oracle.portal.provider.v1.PersonalizationObject} interface (recommended), it
 * will be responsible for reading and writing its own content through its
 * read() and write() methods. Otherwise, it is assumed that the data class
 * implements the <code>Serializable</code> interface, and can therefore be
 * saved and restored through Java serialization.<p>
 *
 * The persisted data is maintained in the filesystem; one data file per user
 * customization. The root directory for this repository should be registered
 * using {@link #setRootDirectory(String)}. Again, this will most commonly be
 * achieved through an entry in the provider.xml file
 * (e.g. <code>&lt;rootDirectory><i>path</i>&lt;/rootDirectory></code>). If
 * this directory is not set explicitly, however, the corresponding servlet's
 * application root, specified via the "provider_root" initArg, will be
 * used.<p>
 *
 * The customization data files for each portlet instance are maintained in
 * subdirectories of this root, with a path of the form
 * "providerId/portletId/portletInstance". Each customization data file has a
 * filename of the form "userName.dat", where userName corresponds to the user
 * for which the customization has been made (or "_default.dat" for the default
 * user).<p>
 *
 * If the 'path hashing' feature has been activated (i.e. {@link
 * #setUseHashing(String)} has been called with a <code>"true"</code> value or
 * <code>&lt;useHashing>true&lt;/useHashing></code> has been specified in
 * provider.xml), then each customization data file for a particular portlet
 * instance is stored in a subdirectory of the instance directory determined by
 * 'hashing' the data file name. This can improve filesystem performance by
 * limiting the number of customization data files stored in a single
 * directory.<p>
 */
public class FilePersonalizationManager implements PortletPersonalizationManager 
{

    private File mRoot = null;
    private Class mDataClass;
    private boolean mUseSerialization = false;
    private boolean mUseHashing = false;
    private boolean mDebug = false;
    private ProviderLog mLogger = null;

    // All customization data filenames for the default user start with one of
    // these Strings (depending on whether serialization is being used)
    private static final String DEFAULT_FILE_PREFIX_SER = "edit_defaults";
    private static final String DEFAULT_FILE_PREFIX_NO_SER = "_default";

    // All customization data filenames end with one of these Strings
    // (depending on whether serialization is being used)
    private static final String FILE_SUFFIX_SER = ".ser";
    private static final String FILE_SUFFIX_NO_SER = ".dat";

    // The radix used for encoding hashed values as strings
    private static final int HASH_BASE = 16;
    // The largest prime number less than HASH_BASE^2
    private static final int HASH_SIZE = 251;

    /**
     * Null Constructor
     */
    public FilePersonalizationManager() 
    {
    }

    /**
     *  Sets the class name of the data objects this instance manages.  This
     *  class must implement <code>PersonalizationObject</code> or
     *  <code>Serializable</code>.  It is recommended that your data object
     *  subclass <code>NameValuePersonalizationObject</code>.
     *
     *  @param className the class name for that class that represents the data
     *  to be managed.
     */
    public void setDataClass(String className) throws PersonalizationException

    {
        Object template = null;
        try
        {
            mDataClass = Class.forName(className);
            template = mDataClass.newInstance();
        }
        catch (ClassNotFoundException e)
        {
          throw new PersonalizationException(e);
        }
        catch (IllegalAccessException e)
        {
          throw new PersonalizationException(e);
        }
        catch (InstantiationException e)
        {
          throw new PersonalizationException(e);
        }

        // Decide up front whether we are going to have to use Java
        // Serialization for the data objects, rather than the
        // PersonalizationObject interface, for backwards compatibility
        if(!(template instanceof PersonalizationObject))
        {
            if(template instanceof Serializable)
            {
                mUseSerialization = true;
            }
            // If the Object doesn't even implement Serializable then we
            // can't use it at all
            else
            {
                throw new PersonalizationException("FilePersonalizationManager Data Class does not implement supported interface.");
            }
        }
    }

    /**
     *  Sets the path to the filesystem directory under which this provider's
     *  personalization data will be stored.
     *
     *  @param path physical path to serve as the root directory for the
     *  personalization data repository.
     */
    public void setRootDirectory(String root)
    {
        File rootFile = new File(root);
        if (!rootFile.isDirectory() || rootFile.toString().equals("/"))
        {
            throw new SecurityException("Invalid root directory specified for FilePersonalizationManager");
        }
        mRoot = rootFile;
    }

    /**
     *  Activate or deactivate the 'path hashing' feature for this
     *  FilePersonalizationManager.
     *
     *  @param useHashing boolean indicating whether the 'path hashing' feature
     *  should be used when determining the filesystem path in which to store a
     *  customization data file for a particular portlet instance.  If false,
     *  then all customization data files for a particular portlet instance
     *  will be stored in a single directory corresponding to that portlet
     *  instance. If true, then an extra level of directories below the portlet
     *  instance directory, with names derived by a hash function of the
     *  customization data file name, will be used in order to limit the number
     *  of customization data files stored in a single directory.
     */
    public void setUseHashing(boolean useHashing)
    {
        mUseHashing = useHashing;
    }
    
    /**
     *  Activate or deactivate the 'path hashing' feature for this
     *  FilePersonalizationManager.
     *
     *  @param useHashing String representation of boolean ("true" or "false")
     *  indicating whether the 'path hashing' feature should be used when
     *  determining the filesystem path in which to store a customization data
     *  file for a particular portlet instance.
     *
     *  @see #setUseHashing(boolean)
     */
    public void setUseHashing(String useHashing)
    {
        setUseHashing(Boolean.valueOf(useHashing).booleanValue());
    }

   /**
     * Initializes a new FilePersonalizationManager instance. This method
     * should be invoked on each of a Provider's PortletPersonalizationManagers
     * whenever the Provider instance is initialized on startup (by the
     * Provider's <code>init()</code> method). Currently, the method defaults
     * the root directory for the FilePersonalizationManager's personalization
     * data to the provider's repository path.
     *
     * @param p the portlet this FilePersonalizationManager is controlling.
     */
    public void initInstance(Portlet p)
    {
        Provider provider = p.getProvider();
        setRootDirectory(provider.getProviderRepositoryPath());
        if (provider instanceof ProviderXtra2)
        {
            ProviderXtra2 px = (ProviderXtra2)provider;
            mLogger = px.getProviderLog();

            // only enable debug if there is a logger
            mDebug = (px.isDebugEnabled() && (mLogger != null));
        }
    }

    /**
     *  Initializes the repository for this FilePersonalizationManager. This
     *  method is invoked on each of a Provider's
     *  PortletPersonalizationManagers whenever the Provider is registered on a
     *  Portal Node (when the Provider's <code>register()</code> method is
     *  called). This gives the personalization manager an opportunity to
     *  initialize the repository for the corresponding Portlet.<p>
     *
     *  The FilePersonalizationManager currently does nothing in response to
     *  this call.
     *
     *  @param p the portlet this FilePersonalizationManager is controlling.
     */
    public void init(Portlet p)
    {
        // do nothing currently
    }

    /**
     *  Called to allow the Portlet Personalization manager to destroy its
     *  repository.  This method is called each time a Provider that manages
     *  this PortletPersonalizationManager's Portlet is deregistered (from a
     *  portal).<p>
     *
     * In response to this call, the FilePersonalizationManager removes all
     * data files from the filesytem that pertain to this Portlet.
     *
     *  @param p the portlet this PortletPersonalizationManager is controlling.
     */
    public void destroy(Portlet p) throws AccessControlException, IOException
    {
        // don't do anything if the portlet doesn't support customizations
        if (!p.hasShowEdit() && !p.hasShowEditDefault())
            return;

        // rename the portlet's personalization directory before attempting
        // to delete the files.  This assures atomic removal of directory
        // and all its contents (assumes long filenames are supported by the OS)
        String[] oldPortletRootComponents = {String.valueOf(p.getProvider().getProviderId()),
                                             String.valueOf(p.getId())};
        File oldPortletRoot = createFileName(oldPortletRootComponents);

        if(oldPortletRoot.exists())
        {
            SimpleStringBuffer newPortletRootName = new SimpleStringBuffer();
            newPortletRootName.append("delete_me_");
            newPortletRootName.append(String.valueOf(System.currentTimeMillis()));
            newPortletRootName.append("_");
            newPortletRootName.append(p.getId());

            String[] newPortletRootComponents = {String.valueOf(p.getProvider().getProviderId()),
                                                 newPortletRootName.toString()};
                                                 
            File newPortletRoot = createFileName(newPortletRootComponents);
            try 
            {
                if (!oldPortletRoot.renameTo(newPortletRoot))
                    throw new AccessControlException("Failed to rename portlet root directory from " +
                                                     oldPortletRoot.toString() +
                                                     " to " +
                                                     newPortletRoot.toString());
            }
            catch (SecurityException se) 
            {
                throw new AccessControlException("Failed to rename portlet root directory from " +
                                                 oldPortletRoot.toString() +
                                                 " to " +
                                                 newPortletRoot.toString());
            }

            // cleanup the renamed directory or file.  Ignore any exceptions that
            // occur during this phase
            deleteFile(newPortletRoot);
        }
    }

    /**
     * Creates the initial personalization reference for this (new)
     * PortletReference and establishes its (static) defaults.<p>
     *
     * In response to this call, the FilePersonalizationManager creates a new
     * instance of the data object class that is registered to it, and calls
     * its <code>init()</code> method if the class implements
     * PersonalizationObject or CustomizationObject.  Finally, the new instance
     * is saved to the filesystem, ensuring the initial data persists.
     *
     * @param ref the PortletReference.  The details of the PortletReference
     * determine whether this creates the default, system instance or a specific
     * user instance.
     * @param user the user attempting this operation.  The manager is expected
     * to authorize the user before completing this operation.
     */
    public void create(PortletReference ref, ProviderUser user)
        throws IOException, PortletAlreadyExistsException, AccessControlException 
    {

        // First get a brand new data object of the appropriate class
        Object o = createDataObject(ref);

        create(ref, o, user);
    }

    /**
     * Creates the initial personalization reference for this (new)
     * PortletReference.  The initial data object representing this new
     * instance is passed as a parameter.<p>
     *
     * In response to this call, the FilePersonalizationManager saves the
     * passed object to the filesystem, ensuring the initial data persists.
     *
     * @param ref the PortletReference.  The details of the PortletReference
     * determine whether this creates the default, system instance or a specific
     * user instance.
     * @param o the Object representing the initial values for this instance
     * (should implement PersonalizationObject or Serializable).
     * @param user the user attempting this operation.  The manager is expected
     * to authorize the user before completing this operation.
     */
    public void create(PortletReference ref, Object o, ProviderUser user)
        throws PortletAlreadyExistsException, AccessControlException,
               IOException 
    {
        checkAuthorization(ref, user);

        //Get the appropriate hashed path for this PortletReference
        File f = getReferencePath(ref);

        if (f.exists()) 
        {
            throw new PortletAlreadyExistsException();
        }

        writeDataObject(o, f, ref);
    }

    /**
     *  Returns true if a customization exists for this given reference.  This
     *  will be true if a create() has been previously called.
     *
     *  @param ref the portlet instance to check
     *  @user the user making this request -- passed to give the manager access
     *  the the session.
     */
    public boolean exists(PortletReference ref, ProviderUser user) throws AccessControlException 
    {
        checkAuthorization(ref, user);
        File f = getReferencePath(ref);
        boolean exists = f.exists();

        if (mDebug)
        {
            SimpleStringBuffer buf = new SimpleStringBuffer(100);
            buf.append("FilePersonalizationManager.exists() - ");
            if (exists)
            {
                buf.append("Customization file exists: ");
            }
            else
            {
                buf.append("Customization file does not exist: ");
            }
            buf.append(f.toString());
            mLogger.log(buf.toString());
        }
        return exists;
    }


    /**
     *  Returns a data object containing the customizations particular to this
     *  reference.  If no customizations exist then the
     *  PortletNotFoundException is thrown.
     *
     *  @param ref the portlet reference that identifies the instance whose
     *  customizations are being requested.
     *  @param user the user attempting this operation.  The manager is expected
     *  to authorize the user before completing this operation.
     *  @return an Object (of the class specified with
     *  <code>setDataClass</code>) containing the customizations particular to
     *  this reference
     *  @exception PortletNotFoundException if no customizations exist
     */
    public Object read(PortletReference ref, ProviderUser user)
        throws PortletNotFoundException, AccessControlException, IOException 
    {

        checkAuthorization(ref, user);

        File f = getReferencePath(ref);

        if (!f.exists()) 
        {
            throw new PortletNotFoundException();
        }

        return readDataObject(f, ref);
    }

    /**
     *  Updates this reference's customization using the passed data object.
     *
     *  @param ref the portlet reference that identifies the instance whose
     *  customizations are being updated.
     *  @param o the data object containing the new values (should implement
     *  PersonalizationObject or Serializable).
     *  @param user the user attempting this operation.  The manager is expected
     *  to authorize the user before completing this operation.
     */
    public void write(PortletReference ref, Object o, ProviderUser user)
        throws java.io.IOException, PortletNotFoundException, AccessControlException 
    {

        checkAuthorization(ref, user);

        File f = getReferencePath(ref);

        if (!f.exists()) 
        {
            throw new PortletNotFoundException();
        }

        writeDataObject(o, f, ref);
    }

    /**
     *  Destroys a given Portlet instance from the repository by reference. If
     *  the reference refers to the system (default) instance the manager
     *  should additionally delete all user customizations that correspond.
     *
     *  @param  ref the portlet reference that identifies the instance whose
     *  customizations are being deleted.
     *  @param user the user attempting this operation.  The manager is expected
     *  to authorize the user before completing this operation.
     */
    public void destroy(PortletReference ref, ProviderUser user)
        throws PortletNotFoundException, AccessControlException, IOException
    {
        checkAuthorization(ref, user);
        String refPath = null;
        File oldFile = null;
        File newFile = null;

        // rename the instance directory or customization file in preparation
        // for deletion to ensure an atomic operation
        if (ref.isDefault())
        {
            oldFile = getReferenceDirectory(ref);

            if(oldFile.exists()) 
            {
                SimpleStringBuffer newName = new SimpleStringBuffer();
                newName.append("delete_me_");
                newName.append(String.valueOf(System.currentTimeMillis()));
                newName.append("_");
                newName.append(ref.getName());

                String[] newNameComponents = {String.valueOf(ref.getProvider().getProviderId()),
                                              String.valueOf(ref.getPortlet().getId()),
                                              newName.toString()};

                newFile = createFileName(newNameComponents);
                try 
                {
                    if (!oldFile.renameTo(newFile)) 
                    {
                        throw new AccessControlException("Failed to rename personalization directory from " +
                                                         oldFile.toString() +
                                                         " to " +
                                                         newFile.toString());
                    }
                }
                catch (SecurityException se) 
                {
                    throw new AccessControlException("Failed to rename personalization directory from " +
                                                     oldFile.toString() +
                                                     " to " +
                                                     newFile.toString());
                }

                // cleanup the renamed directory or file.  Ignore any
                // exceptions that occur during this phase
                deleteFile(newFile);
            }
        }
        else
        {
            // if oldFile does not exist, there are no personalizations for the
            // referencePath
            oldFile = getReferencePath(ref);

            if(oldFile.exists())
            {
                String username = ref.getUserCustomization().getName();

                SimpleStringBuffer newName = new SimpleStringBuffer();
                newName.append("delete_me_");
                newName.append(String.valueOf(System.currentTimeMillis()));
                newName.append("_");

                // encode the username because it may contain multibyte chars
                newName.append(MultiByteEncoder.encodeUTF(username));
                newName.append(mUseSerialization ? FILE_SUFFIX_SER : FILE_SUFFIX_NO_SER);

                String[] newFileComponents = {newName.toString()};
                newFile = createFileName(new File(oldFile.getParent()),
                                         newFileComponents);
                try 
                {
                    if (!oldFile.renameTo(newFile))
                        throw new AccessControlException("Failed to rename personalization file from " +
                                                         oldFile.toString() +
                                                         " to " +
                                                         newFile.toString());
                }
                catch (SecurityException se) 
                {
                    throw new AccessControlException("Failed to rename personalization file from " +
                                                     oldFile.toString() +
                                                     " to " +
                                                     newFile.toString());
                }

                // cleanup the renamed directory or file.  Ignore any
                // exceptions that occur during this phase
                deleteFile(newFile);

            } // if oldFile exists
        }
    }

    /**
     *  Copies the instance customizations from one reference to another.  If
     *  the customization for the to reference doesn't exist it is created else
     *  the current values are overwritten.
     *
     *  @param from reference for the portlet instance we are copying from.
     *  @param to reference for the portlet instance we are copying to.
     *  @param user the user attempting this operation.  The manager is expected
     * to authorize the user before completing this operation.
     */
    public void copy(PortletReference from, PortletReference to, ProviderUser user) throws PortletNotFoundException,
    AccessControlException,
    IOException
    {
        checkAuthorization(from, user);

        if (!exists(from, user))
            throw new PortletNotFoundException();

        // from and to should either be both default references or both
        // non-default references
        if(from.isDefault() != to.isDefault())
        {
            throw new AccessControlException("PortletReference mismatch");
        }

        // If we are copying for the default user, we copy both the default
        // language default customization and the language-specific default
        // customizations
        if(from.isDefault())
        {
            // Search for all default and language default customizations for
            // given portlet reference
            Enumeration i = findDefaults(from);
            while(i.hasMoreElements())
            {
                File fromFile = (File)i.nextElement();
                File toFile;

                // First, work out which user the file corresponds to, and
                // generate a new reference path for it
                String fromName = fromFile.getName();

                // If the 'path hashing' feature is activated, generate a
                // subdirectory of the reference directory corresponding to the
                // 'hashed' value of the filename
                if(mUseHashing)
                {
                    String[] pathComponents = {hashName(fromName), fromName};
                    toFile = createFileName(getReferenceDirectory(to), pathComponents);
                }
                else
                {
                    String[] pathComponents = {fromName};
                    toFile = createFileName(getReferenceDirectory(to), pathComponents);
                }

                // create the destination directory
                File parentDirectory = new File(toFile.getParent());
                parentDirectory.mkdirs();

                //copy the file
                copyFile(fromFile, toFile);
            }
        } 
        // Otherwise, we just copy the data for the specific reference
        else
        {
            File fromFile = getReferencePath(from);
            File toFile = getReferencePath(to);

            // create the destination directory
            File parentDirectory = new File(toFile.getParent());
            parentDirectory.mkdirs();

            // copy the file
            copyFile(fromFile, toFile);
        }
    }


    private void copyFile(File src, File dest) throws IOException
    {
        FileInputStream fis = new FileInputStream(src);
        FileOutputStream fos = new FileOutputStream(dest);
        BufferedInputStream is = new BufferedInputStream(fis);
        BufferedOutputStream os = new BufferedOutputStream(fos);
        int maxlen = 1024;
        int offset = 0;
        int bytesRead = maxlen;
        byte[] buffer = new byte[maxlen];
        try 
        {
            while(bytesRead == maxlen) 
            {
                bytesRead = is.read(buffer, offset, maxlen);
                os.write(buffer, offset, bytesRead);
                offset += bytesRead;
            }
        }
        catch (IOException ioe) 
        {
            throw ioe;
        }
        finally 
        {
            // close I/O streams and release resources.
            try 
            {
                if (is != null)
                    is.close();
                if (fis != null)
                    fis.close();
                if (os != null)
                    os.close();
                if (fos != null)
                    fos.close();
            }
            catch (Exception e) 
            {
                // do nothing
            }
        }
    }

    private void checkAuthorization(PortletReference ref, ProviderUser user) throws AccessControlException 
    {
        PortletSecurityManager security = ref.getPortlet().getSecurityManager();

        try 
        {
            if (security != null && !security.hasAccess(ref, user)) 
            {
                throw new AccessControlException();
            }
        }
        catch(PortletNotFoundException e) 
        {
            // unexpected
            throw new AccessControlException();
        }
    }

    private File getReferenceDirectory(PortletReference ref)
    {
        String[] refDirComponents = {String.valueOf(ref.getProvider().getProviderId()),
                                     String.valueOf(ref.getPortlet().getId()),
                                     ref.getName()};
        return createFileName(refDirComponents);
    }

    private File getReferencePath (PortletReference ref)
    {
        // Derive the filename, named after the user
        SimpleStringBuffer fileNameBuff = new SimpleStringBuffer();
        if (ref.isDefault()) 
        {
            // Use 'old style' name for default instance if using Java
            // Serialization
            fileNameBuff.append(mUseSerialization ? DEFAULT_FILE_PREFIX_SER : DEFAULT_FILE_PREFIX_NO_SER);
            Locale l = ref.getLocale(), d = ref.getDefaultLocale();
            if (!(l != null && d != null && l.equals(d))) 
            {
                fileNameBuff.append("_");
                fileNameBuff.append(getLanguagePath(l));
            }
        }
        else
        {
            // encode the username because it may contain mb chars
            String username = ref.getUserCustomization().getName();
            fileNameBuff.append(MultiByteEncoder.encodeUTF(username));
        }
        // Use ".ser" suffix if using Java Serialization
        fileNameBuff.append(mUseSerialization ? FILE_SUFFIX_SER : FILE_SUFFIX_NO_SER);
        String fileName = fileNameBuff.toString();

        // If the 'path hashing' feature is activated, generate a subdirectory
        // of the reference directory corresponding to the 'hashed' value of
        // the filename
        if(mUseHashing)
        {
            String[] pathComponents = {hashName(fileName), fileName};
            return createFileName(getReferenceDirectory(ref), pathComponents);
        }
        else
        {
            String[] pathComponents = {fileName};
            return createFileName(getReferenceDirectory(ref), pathComponents);
        }

    }

    // Search the reference directory for the given portlet reference for
    // default customization data files (possibly under hash subdirectories)
    private Enumeration findDefaults(PortletReference ref)
    {
        Vector found = new Vector();
        File f = getReferenceDirectory(ref);
        if(mUseHashing)
        {
            String[] dirsToSearch = f.list();
            for(int i=0; i<dirsToSearch.length; i++)
            {
                File f1 = new File(f, dirsToSearch[i]);
                if(f1.isDirectory())
                {
                    findDefaults(f1, found);
                }
            }
        }
        else if(f.isDirectory())
        {
            findDefaults(f, found);
        }
        return found.elements();
    }

    // Search the given directory for default customization data files
    private void findDefaults(File f, Vector found)
    {
        // All customization data filenames for the default user start with this string        
        String defaultPrefix = (mUseSerialization ? DEFAULT_FILE_PREFIX_SER : DEFAULT_FILE_PREFIX_NO_SER);
        // All customization data filenames end with this string        
        String defaultSuffix = (mUseSerialization ? FILE_SUFFIX_SER : FILE_SUFFIX_NO_SER);
        // All customization data filenames for the LANGUAGE default user start with this string
        String languageDefaultPrefix = defaultPrefix + "_";
        // All customization data filenames for the default user equal this string
        String defaultName = defaultPrefix + defaultSuffix;

        String[] filesToSearch = f.list();
        for(int i=0; i<filesToSearch.length; i++)
        {
            File f1 = new File(f, filesToSearch[i]);
            if(f1.isFile() && f1.canRead() &&
               (filesToSearch[i].equals(defaultName) ||
                filesToSearch[i].startsWith(languageDefaultPrefix) &&
                filesToSearch[i].endsWith(defaultSuffix)))
            {
                found.addElement(f1);
            }
        }
    }
        
    private String getLanguagePath(Locale l)
    {
        String language = l.getLanguage();
        String country = l.getCountry();

        if (country == null || country.length() == 0) 
        {
            return language;
        }
        else
        {
            return language + "_" + country;
        }
    }

    private boolean createPath(String path) 
    {
        boolean success = true;
        File classDir = new File(path);
        if (!classDir.exists()) 
        {
            try 
            {
                success = classDir.mkdirs();
            }
            catch(SecurityException se) 
            {
                success = false;
            }
        }
        return success;
    }


    private FileOutputStream getFileOutputStream(File out) throws FileNotFoundException, IOException 
    {
        FileOutputStream os;
        try 
        {
            os = new FileOutputStream(out);
        }
        catch(FileNotFoundException fnf) 
        {
            // create directories and retry
            if(!createPath(out.getParent())) 
            {
                throw new IOException("Failed to create directory path " + out.getParent());
            }
            os = new FileOutputStream(out);
        }

        return os;
    }

    private void deleteFile(File targetFile)
    {
        try
        {
            if (targetFile.isDirectory())
            {
                // get a list of all the files in the directory
                String [] directoryList = targetFile.list();
                File temp = null;
                if (directoryList != null) 
                {
                    for (int i=0; i < directoryList.length; i++) 
                    {
                        temp = new File(targetFile, directoryList[i]);
                        deleteFile(temp);
                    }
                }
            }
            targetFile.delete();
        }
        catch (SecurityException se) 
        {
            // do not re-throw the exception
        }
    }

    private String getTempPath(String path) 
    {
        SimpleStringBuffer buf = new SimpleStringBuffer(200);
        buf.append(path);
        buf.append("_");
        buf.append(System.currentTimeMillis());
        return buf.toString();
    }

    private void deleteTemp(String tempPath) 
    {
        deleteTemp(new File(tempPath));
    }

    private void deleteTemp(File f) 
    {
        if (f.exists()) 
        {
            f.delete();
        }
    }

    private void commitTemp(File src, File dest) throws IOException 
    {
        if (!src.exists()) return;

        for (int i = 1; i < 5; i++) 
        {
            if (doCommit(src, dest)) return;

            // back off and try again
            try 
            {
                Thread.sleep(i*15);
            }
            catch(InterruptedException ie) 
            {
                // just continue on
            }
        }
        throw new IOException("Unable to commit changes.  Please try again.");
    }

    private boolean doCommit(File src, File dest) 
    {

        if (!src.renameTo(dest)) 
        {
            dest.delete();
            return src.renameTo(dest);
        }

        return true;
    }

    // maps filename string string to a two character hexadecimal string, e.g. "b6"
    private String hashName(String name) 
    {
        // Use Math.abs in case we get a negative value back from hashCode()
        int hashValue = Math.abs(name.hashCode() % HASH_SIZE);
        return Integer.toString(hashValue, HASH_BASE);
    }

    // Support saving and restoring of data objects implementing either
    // PersonalizationObject or Serializable

    // For an 'atomic' cache operation, create/write to a temporary file and
    // then overwrite
    private void writeDataObject(Object o, File f, PortletReference ref)
        throws IOException
    {
        File t = new File(getTempPath(f.toString()));

        OutputStream os = null;
        try 
        {
            os = getFileOutputStream(t);
            if(!mUseSerialization)
            {
                PersonalizationObject po = (PersonalizationObject)o;
                // make sure the reference path is correctly set for this entry
                po.setReferencePath(getReferencePath(ref).toString());
                po.write(os);
            }
            else
            {
                ObjectOutputStream oos = new ObjectOutputStream(os);
                oos.writeObject(o);
            }
        }
        catch (Exception e) 
        {
            try 
            {
                if (os != null)
                    os.close();
                deleteTemp(t);
            }
            catch (Exception e2) 
            {
                // do nothing
            }
            if (mLogger != null)
            {
                SimpleStringBuffer buf = new SimpleStringBuffer(200);
                buf.append("Error writing ");
                buf.append(f.toString());
                buf.append(" : ");
                buf.append(e.getMessage());
                mLogger.log(buf.toString());
            }
            e.printStackTrace();
            throw new IOException(e.getMessage());
        }

        os.close();
        commitTemp(t, f);

    }

    private Object readDataObject(File f, PortletReference ref)
        throws IOException
    {
        Object o = null;
        InputStream is = null;
        try 
        {
            is = new FileInputStream(f);
            if(!mUseSerialization)
            {
                // Get a brand new data object of the appropriate class
                o = createDataObject(ref);
                ((PersonalizationObject)o).read(is);
            }
            else
            {
                ObjectInputStream ois = new ObjectInputStream(is);
                o = ois.readObject();
            }
        }
        catch(Exception e) 
        {
            if (mLogger != null)
            {
                SimpleStringBuffer buf = new SimpleStringBuffer(200);
                buf.append("Error reading ");
                buf.append(f.toString());
                buf.append(" : ");
                buf.append(e.getMessage());
                mLogger.log(buf.toString());
            }
            e.printStackTrace();
            throw new IOException(e.getMessage());
        }
        finally
        {
            try 
            {
                if (is != null) is.close();
            }
            catch (Exception e) 
            {
                // do nothing
            }
        }
        return o;
    }

    private Object createDataObject(PortletReference ref) throws IOException 
    {
        Object o = null;
        try
        {
            o = mDataClass.newInstance();
            if(!mUseSerialization)
            {
                PersonalizationObject po = (PersonalizationObject)o;
                po.init(ref);
                po.setReferencePath(getReferencePath(ref).toString());
            }
            // Provide backwards compatibility by supporting data objects
            // implementing old CustomizationObject interface
            else if(o instanceof CustomizationObject)
            {
                ((CustomizationObject)o).init(ref);
            }
        }
        catch(Exception e) 
        {
            if (mLogger != null)
            {
                SimpleStringBuffer buf = new SimpleStringBuffer(200);
                buf.append("Error instantiating ");
                buf.append(mDataClass.getName());
                buf.append(" : ");
                buf.append(e.getMessage());
                mLogger.log(buf.toString());
            }
            e.printStackTrace();
            throw new IOException(e.getMessage());
        }
        return o;
    }

    // Ensure the components of a file path all have valid names, i.e. don't
    // contain File separator characters and aren't named after current or
    // parent dirs
    protected static boolean isValidPathComponent(String component)
    {
        return component != null &&
            component.length() > 0 &&
            component.indexOf(File.separator) == -1 &&
            component.indexOf("/") == -1 &&
            !component.equals(".") &&
            !component.equals("..");
    }

    private File createFileName(String[] components)
    {
        return createFileName(mRoot, components);
    }

    private File createFileName(File root, String[] components)
    {
        SimpleStringBuffer path = new SimpleStringBuffer();
        for(int i=0; i<components.length; i++)
        {
            String component = components[i];
            if(!isValidPathComponent(component))
            {
                throw new SecurityException("FilePersonalizationManager error: Invalid component '" +
                                            component + "' in File path");
            }
            if (path.length() > 0 &&
                path.charAt(path.length() - 1) != File.separatorChar)
            {
                path.append(File.separator);
            }
            path.append(component);
        }
        return new File(root, path.toString());
    }
}
