/* Copyright (c) 2001, Oracle Corporation. All rights reserved. */
package oracle.portal.provider.v2.preference;

import java.io.*;
import java.util.*;
import oracle.webdb.utils.SimpleStringBuffer;
import oracle.webdb.utils.HTTPUtils;
import oracle.portal.log.LogManager;
import oracle.portal.provider.v2.Validateable;
import oracle.portal.provider.v2.utils.ValidationError;


/**
 * A <code>PreferenceStore</code> is a generic storage mechanism for loading
 * and saving data items held in {@link PreferenceDataObject}s. Data is stored
 * under a 'key' represented by a {@link Preference} whose primary components
 * are a 'reference path' and a 'preference type'. See the {@link Preference}
 * class for more details.<p>
 *
 * Note that a reference path, as returned by {@link
 * Preference#getReferencePath}, is a standard Java unicode String, and as such
 * may contain multibyte characters. Implementations of
 * <code>PreferenceStore</code> may therefore have to encode the String into a
 * format more suitable for the underlying storage mechanism.
 */
public abstract class PreferenceStore implements Validateable
{
    /**
     * The character used betwen path components in reference paths.
     */
    public static final char PATH_SEPARATOR_CHAR = '/';

    /**
     * String containing the single character used betwen path components in
     * reference paths.
     */
    public static final String PATH_SEPARATOR_STRING = "/";

    /**
     * Path type used for storing user level portlet customizations. For
     * backward compatibility with older versions of PDK-Java, this path type
     * must be given special treatment by the default
     * <code>PreferenceStore</code> implementations, hence the reason why it is
     * defined at this level.
     */
    public static final String USER_CUSTOMIZATION_TYPE = "oracle.portal.provider.v2.user";

    /**
     * Path type used for storing system default and locale default level
     * portlet customizations. For backward compatibility with older versions
     * of PDK-Java, this path type must be given special treatment by the
     * default <code>PreferenceStore</code> implementations, hence the reason
     * why it is defined at this level.
     */
    public static final String DEFAULT_CUSTOMIZATION_TYPE = "oracle.portal.provider.v2.default";

    protected static final int READ_BUFFER_SIZE = 1024;   

    protected String mName;

    public PreferenceStore()
    {
    }

    /**
     * Sets the name that should be used to identify this
     * <code>PreferenceStore</code>.
     * @param name name that should be used to identify this
     * <code>PreferenceStore</code>.
     */ 
    public void setName(String name)
    {
        mName = name;
    }

    /**
     * Gets the name that should be used to identify this
     * <code>PreferenceStore</code>.
     * @return the name that should be used to identify this
     * <code>PreferenceStore</code>.
     */
    public final String getName()
    {
        return mName;
    }

    /**
     * Gets an <code>Iterator</code> over all of the {@link Preference} objects
     * in this <code>PreferenceStore</code> stored under the given context
     * path. Note that this listing is 'recursive', in that
     * <code>Preference</code>s stored in context paths below the given one
     * will also be returned. Provided as shorthand for {@link #list(String,
     * List) list(context, null)}.
     * @param context the context path under which <code>Preference</code>s
     * should be listed.
     * @exception PreferenceStoreException if an error occurs during the
     * operation.
     */
    public Iterator list(String context)
        throws PreferenceStoreException
    {
        return list(context, null);
    }

    /**
     * Gets an <code>Iterator</code> over all of the {@link Preference} objects
     * in this <code>PreferenceStore</code> stored under the given context path
     * with one of the given preference types. Note that this listing is
     * 'recursive', in that <code>Preference</code>s stored in context paths
     * below the given one will also be returned. Note also that the data
     * object is not loaded into any of the listed <code>Preference</code>s.
     * @param context the context path under which <code>Preference</code>s
     * should be listed.
     * @param preferenceTypes a list of strings, naming the preference types to
     * include, or <code>null</code> if all preference types should be
     * included.
     * @exception PreferenceStoreException if an error occurs during the
     * operation.
     */
    public abstract Iterator list(String context,
                                  List preferenceTypes)
        throws PreferenceStoreException;


    /**
     * Copies the data from all the {@link Preference} objects in this
     * <code>PreferenceStore</code> with the given source context path to the
     * given destination context path. Note that this copy is 'recursive', in
     * that <code>Preference</code>s stored in context paths below the given
     * source context path will also be copied (with new context paths
     * calculated relative to the given destination context path).  Provided as
     * shorthand for {@link #copy(String, String, boolean, List)
     * copy(sourceContext, destContext, overwrite, null)}.
     * @param sourceContext the source context path from which
     * <code>Preference</code>s should be copied.
     * @param destContext the destination context path to which
     * <code>Preference</code>s should be copied.
     * @param overwrite boolean indicating whether preferences existing under
     * the destination context path should be overwritten.
     * @param preferenceTypes a list of strings, naming the preference types to
     * include, or <code>null</code> if all preference types should be
     * included.
     * @exception EntryAlreadyExistsException if <code>overwrite</code> was
     * false and one or more <code>Preference</code>s were found to already
     * exist under their destination context paths.
     * @exception PreferenceStoreException if any other error occurs during the
     * operation.
     */
    public void copy(String sourceContext,
                     String destContext,
                     boolean overwrite)
        throws EntryAlreadyExistsException,
               PreferenceStoreException
    {
        copy(sourceContext, destContext,
             overwrite, null);
    }

    /**
     * Copies the data from all the {@link Preference} objects in this
     * <code>PreferenceStore</code> with the given source context path and one
     * of the given preference types to the given destination context
     * path. Note that this copy is 'recursive', in that
     * <code>Preference</code>s stored in context paths below the given source
     * context path will also be copied (with new context paths calculated
     * relative to the given destination context path).
     * @param sourceContext the source context path from which
     * <code>Preference</code>s should be copied.
     * @param destContext the destination context path to which
     * <code>Preference</code>s should be copied.
     * @param overwrite boolean indicating whether preferences existing under
     * the destination context path should be overwritten.
     * @param preferenceTypes a list of strings, naming the preference types to
     * include, or <code>null</code> if all preference types should be
     * included.
     * @exception EntryAlreadyExistsException if <code>overwrite</code> was
     * false and one or more <code>Preference</code>s were found to already
     * exist under their destination context paths.
     * @exception PreferenceStoreException if any other error occurs during the
     * operation.
     */
    public void copy(String sourceContext,
                     String destContext,
                     boolean overwrite,
                     List preferenceTypes)
        throws EntryAlreadyExistsException,
               PreferenceStoreException
    {
        Iterator sourcePrefs = list(sourceContext, preferenceTypes);
        int sourceContextLen = sourceContext.length();
        Preference sourcePref, destPref;
        while (sourcePrefs.hasNext())
        {
            sourcePref = (Preference)sourcePrefs.next();
            String sourcePath = sourcePref.getContextPath();
            String destPath = destContext;
            // Qualify the destination path with the same sub paths as the
            // source
            if(sourcePath.length() > sourceContextLen)
            {
                destPath += sourcePath.substring(sourceContextLen);
            }
            destPref = new Preference(destPath,
                                      sourcePref.getName(),
                                      sourcePref.getType(),
                                      sourcePref.getDataClassName());
            copy(sourcePref, destPref, overwrite);
        }
    }

    /**
     * Destroys all of the {@link Preference} objects in this
     * <code>PreferenceStore</code> stored under the given context path. Note
     * that this operation is 'recursive', in that <code>Preference</code>s
     * stored in context paths below the given one will also be
     * destroyed. Provided as shorthand for {@link #destroy(String,List)
     * destroy(context, null)}.
     * @param context the context path under which <code>Preference</code>s
     * should be listed.
     * @param preferenceTypes a list of strings, naming the preference types to
     * include, or <code>null</code> if all preference types should be
     * included.
     * @exception PreferenceStoreException if an error occurs during the
     * operation.
     */
    public void destroy(String context)
        throws PreferenceStoreException
    {
        destroy(context, null);
    }


    /**
     * Destroys all of the {@link Preference} objects in this
     * <code>PreferenceStore</code> stored under the given context path with
     * one of the given preference types. Note that this operation is
     * 'recursive', in that <code>Preference</code>s stored in context paths
     * below the given one will also be destroyed.
     * @param context the context path under which <code>Preference</code>s
     * should be listed.
     * @param preferenceTypes a list of strings, naming the preference types to
     * include, or <code>null</code> if all preference types should be
     * included.
     * @exception PreferenceStoreException if an error occurs during the
     * operation.
     */
    public abstract void destroy(String context,
                                 List preferenceTypes)
        throws PreferenceStoreException;

    /**
     * Returns <code>true</code> if persisted data exists for the given
     * <code>Preference</code>.
     * @return <code>true</code> if persisted data exists for the given
     * <code>Preference</code>, <code>false</code> otherwise.
     */
    public abstract boolean exists(Preference pref)
        throws PreferenceStoreException;

    /**
     * Loads a {@link PreferenceDataObject} into the given
     * <code>Preference</code>.
     * @exception EntryNotFoundException if no data for the given
     * <code>Preference</code> exists.
     * @exception PreferenceStoreException if any other error occurs during the
     * operation.
     */
    public void read(Preference pref)
        throws EntryNotFoundException,
               PreferenceStoreException
    {
        Class dataClass = pref.getDataClass();

        // Create an instance of the data object and read its data in
        PreferenceDataObject data;
        try
        {
            data = (PreferenceDataObject)dataClass.newInstance();
        }
        catch(Exception e)
        {
            throw new PreferenceStoreException(null, e.getMessage(), null, e);
        }

        InputStream is = getInputStream(pref);
        try
        {
            data.read(is);
            pref.setData(data);
            is.close();
        }
        catch(Exception e)
        {
            try
            {
                is.close();
            }
            catch (Exception e1)
            {
            }            
            throw new PreferenceStoreException(null, e.getMessage(), null, e);
        }
    }

    /**
     * Gets an <code>InputStream</code> for reading the data of the given
     * <code>Preference</code>.<p> 
     *
     * This method is provided in conjunction with {@link #getOutputStream} as
     * a way of streamlining the process of copying data between
     * <code>PreferenceStore</code>s, since data can be copied directly from
     * the <code>InputStream</code> of one <code>Preference</code> to the
     * <code>OutputStream</code> of another, without having to instantiate a
     * <code>PreferenceDataObject</code>.
     * @exception EntryNotFoundException if no data for the given
     * <code>Preference</code> exists.
     * @exception PreferenceStoreException if any other error occurs during the
     * operation.
     */
    public abstract InputStream getInputStream(Preference pref)
        throws EntryNotFoundException,
               PreferenceStoreException;

    /**
     * Saves the {@link PreferenceDataObject} in the given
     * <code>Preference</code> to storage.
     * @param overwrite boolean indicating whether data already existing for
     * this <code>Preference</code> should be overwritten.
     * @exception EntryAlreadyExistsException if <code>overwrite</code> was
     * <code>false</code> and data for the given <code>Preference</code>
     * already exists in storage.
     * @exception PreferenceStoreException if any other error occurs during the
     * operation.
     */
    public void write(Preference pref,
                      boolean overwrite)
        throws EntryAlreadyExistsException,
               PreferenceStoreException
    {
        PreferenceDataObject data = pref.getData();
        if(data == null)
        {
            SimpleStringBuffer exc = new SimpleStringBuffer(200);
            exc.append("No data to write for Preference \"");
            exc.append(pref.getReferencePath());
            exc.append("\"");
            throw new PreferenceStoreException(null, exc.toString());
        }
        OutputStream os = getOutputStream(pref, overwrite);
        try
        {
            data.write(os);
            os.close();
        }
        catch(Exception e)
        {
            try
            {
                os.close();
            }
            catch (Exception e1)
            {
            }            
            throw new PreferenceStoreException(null, e.getMessage(), null, e);
        }
    }

    /**
     * Gets an <code>OutputStream</code> for writing the data of the given
     * <code>Preference</code> directly into storage.<p> 
     *
     * This method is provided in conjunction with {@link #getInputStream} as a
     * way of streamlining the process of copying data between
     * <code>PreferenceStore</code>s, since data can be copied directly from
     * the <code>InputStream</code> of one <code>Preference</code> to the
     * <code>OutputStream</code> of another, without having to instantiate a
     * <code>PreferenceDataObject</code>.
     * @exception EntryAlreadyExistsException if <code>overwrite</code> was
     * <code>false</code> and data for the given <code>Preference</code>
     * already exists in storage.
     * @exception PreferenceStoreException if any other error occurs during the
     * operation.
     */
    public abstract OutputStream getOutputStream(Preference pref,
                                                 boolean overwrite)
        throws EntryAlreadyExistsException,
               PreferenceStoreException;

    /**
     * Copies the data persisted for the given source <code>Preference</code>
     * to the given destination <code>Preference</code>.
     * @param sourcePref the <code>Preference</code> to copy data from.
     * @param destPref the <code>Preference</code> to copy data to.
     * @param overwrite boolean indicating whether data already existing for
     * <code>destPref</code> should be overwritten.
     * @exception EntryAlreadyExistsException if <code>overwrite</code> was
     * false and data was found to already exist for <code>destPref</code>.
     * @exception PreferenceStoreException if any other error occurs during the
     * operation.
     */
    public void copy(Preference sourcePref,
                     Preference destPref,
                     boolean overwrite)
        throws EntryAlreadyExistsException,
               PreferenceStoreException
    {
        InputStream sourceData = getInputStream(sourcePref);
        OutputStream destData = null;
        try
        {
            destData = getOutputStream(destPref, overwrite);
            byte[] readBuff = new byte[READ_BUFFER_SIZE];
            int bytesRead;
            while((bytesRead = sourceData.read(readBuff, 0, READ_BUFFER_SIZE)) != -1)
            {
                destData.write(readBuff, 0, bytesRead);
            }
            sourceData.close();
            destData.close();
        }
        catch(Exception e)
        {
            try
            {
                sourceData.close();
            }
            catch(Exception e1)
            {
            }
            try
            {
                if(destData != null)
                {
                    destData.close();
                }
            }
            catch(Exception e1)
            {
            }
            throw new PreferenceStoreException(null, e.getMessage(), null, e);
        }
    }

    /**
     * Destroys the data persisted for the given <code>Preference</code>.
     * @param pref the <code>Preference</code> whose persisted data should be
     * destroyed.
     * @exception PreferenceStoreException if an error occurs during the
     * operation.
     */
    public abstract void destroy(Preference pref)
        throws PreferenceStoreException;


    /**
     * Validates the internal state of this <code>PreferenceStore</code>
     * instance. Errors found are reported using the supplied {@link
     * oracle.portal.provider.v2.utils.ValidationError} instance.
     *
     * @param context The context of the calling instance.
     * @param errors The current list of errors.
     */
    public void validate(String context, ValidationError errors)
    {
        String ctx =
            ValidationError.newContext(context, "PreferenceStore", getName());

        // Check the validity of the member variables.
        if (mName == null)
        {
            errors.addError(ctx, "PreferenceStore must have a name", null);
        }
    }

    // Encode the given reference path, and also validate that all its
    // components are 'well formed'
    protected String encodePath(String referencePath)
        throws PreferenceStoreException
    {
        StringTokenizer st = new StringTokenizer(referencePath,
                                                 PreferenceStore.PATH_SEPARATOR_STRING,
                                                 true);
        SimpleStringBuffer pathBuff = new SimpleStringBuffer();
        while(st.hasMoreTokens())
        {
            String token = st.nextToken();
            if(token.equals(PreferenceStore.PATH_SEPARATOR_STRING))
            {
                pathBuff.append(token);
            }
            else
            {
                if(!isValidPathComponent(token, st.hasMoreTokens()))
                {
                    SimpleStringBuffer exc = new SimpleStringBuffer(200);
                    exc.append("Invalid reference path: \"");
                    exc.append(referencePath);
                    exc.append("\"");
                    throw new PreferenceStoreException(null, exc.toString());
                }
                pathBuff.append(HTTPUtils.encodeUTF(token));
            }
        }
        return pathBuff.toString();                
    }

    protected String decodePath(String encodedPath)
    {
        StringTokenizer st = new StringTokenizer(encodedPath,
                                                 PreferenceStore.PATH_SEPARATOR_STRING,
                                                 true);
        SimpleStringBuffer pathBuff = new SimpleStringBuffer();
        while(st.hasMoreTokens())
        {
            String token = st.nextToken();
            if(token.equals(PreferenceStore.PATH_SEPARATOR_STRING))
            {
                pathBuff.append(token);
            }
            else
            {
                pathBuff.append(HTTPUtils.decodeUTF(token));
            }
        }
        return pathBuff.toString();                
    }

    // 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. By convention, we only allow the last path component to be
    // 'empty'
    protected static boolean isValidPathComponent(String component, boolean isLast)
    {
        return component != null &&
            (isLast || component.length() > 0) &&
            component.indexOf(File.separatorChar) == -1 &&
            component.indexOf(PreferenceStore.PATH_SEPARATOR_CHAR) == -1 &&
            !component.equals(".") &&
            !component.equals("..");
    }

}
