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

import java.io.*;
import java.util.*;
import oracle.portal.log.LogManager;
import oracle.webdb.utils.SimpleStringBuffer;

/**
 * A <code>Preference</code> is the 'key' through which {@link
 * PreferenceDataObject}s are stored and retrieved from a {@link
 * PreferenceStore}. The main component of this key is the <i>reference
 * path</i>. A reference path is a string consisting of zero or more <i>path
 * components</i>, each delimited by a <code>'/'</code> character. A path
 * component is a string of one or more unicode characters, not including the
 * <code>'/'</code> character. It is illegal for a path component to be equal
 * to the strings <code>"."</code> or <code>".."</code>.<p>
 * 
 * The idea is that a data item associated with a particular application object
 * is stored under a reference path composed from a 'context' reference
 * path which uniquely identifies the object within the context of its 'parent'
 * objects, appended with a path component which uniquely identifies the data
 * item. An example form of reference path corresponding to user-specific data
 * associated with a portlet instance might be <code>"&lt;provider id>/&lt;portlet
 * id>/&lt;portlet instance name>/&lt;user name>"</code>.<p>
 *
 * To prevent one reference path naming scheme from 'clashing' with another in
 * the preference store, the <code>Preference</code> key contains an additional
 * <i>preference type</i> component, which should group together all reference
 * paths of the same 'type'. A naming scheme similar to the one used for Java
 * package names should be used to ensure uniqueness of path types,
 * e.g. <code>"oracle.portal.provider.v2.user"</code>.<p>
 *
 * The final component of the <code>Preference</code> key is the class name of
 * the <code>PreferenceDataObject</code> to be stored or retrieved. This
 * component is only mandatory if the <code>Preference</code> is to be read
 * from storage, as it can be automatically determined for data objects
 * provided for writing to storage.<p>
 */
public final class Preference
{
    private String mContextPath;
    private String mName;
    private String mType;
    private String mReferencePath;
    private String mDataClassName;
    private Class  mDataClass;
    private PreferenceDataObject mDataObject;

    /**
     * Constructs a <code>Preference</code> for reading data with the given
     * class.
     * @param contextPath the context path of this <code>Preference</code>.
     * @param name the name which identifies the preference within the context
     * path.
     * @param type the preference type. Should be a String name which follows
     * the conventions for naming Java packages,
     * e.g. <code>"oracle.portal.provider.v2.user"</code>.
     * @param dataClassName the full class name of the {@link
     * PreferenceDataObject} stored under this <code>Preference</code>.
     */
    public Preference(String contextPath,
                      String name,
                      String type,
                      String dataClassName)
        throws PreferenceStoreException
    {
        initPath(contextPath, name, type);
        setDataClass(dataClassName);
    }

    /**
     * Constructs a <code>Preference</code> for reading data with the given
     * class.
     * @param referencePath the full reference path of this
     * <code>Preference</code>, including a context path followed by a unique
     * name.
     * @param type the preference type. Should be a String name which follows
     * the conventions for naming Java packages,
     * e.g. <code>"oracle.portal.provider.v2.user"</code>.
     * @param dataClassName the full class name of the {@link
     * PreferenceDataObject} stored under this <code>Preference</code>.
     */
    public Preference(String referencePath,
                      String type,
                      String dataClassName)
        throws PreferenceStoreException
    {
        initPath(referencePath, type);
        setDataClass(dataClassName);
    }

    /**
     * Constructs a <code>Preference</code> for writing the given data object.
     * @param contextPath the context path of this <code>Preference</code>.
     * @param name the name which identifies the preference within the context
     * path.
     * @param type the preference type. Should be a String name which follows
     * the conventions for naming Java packages,
     * e.g. <code>"oracle.portal.provider.v2.user"</code>.
     * @param data the data object to be stored.
     */
    public Preference(String contextPath,
                      String name,
                      String type,
                      PreferenceDataObject data)
        throws PreferenceStoreException
    {
        initPath(contextPath, name, type);
        setData(data);
    }

    /**
     * Constructs a <code>Preference</code> for writing the given data object.
     * @param referencePath the full reference path of this
     * <code>Preference</code>, including a context path followed by a unique
     * name.
     * @param type the preference type. Should be a String name which follows
     * the conventions for naming Java packages,
     * e.g. <code>"oracle.portal.provider.v2.user"</code>.
     * @param data the data object to be stored.
     */
    public Preference(String referencePath,
                      String type,
                      PreferenceDataObject data)
        throws PreferenceStoreException
    {
        initPath(referencePath, type);
        setData(data);
    }

    /**
     * Gets the full reference path of this <code>Preference</code>, consisting
     * of a a context path followed by a unique name.
     * @return the full reference path of this <code>Preference</code>
     */
    public String getReferencePath()
    {
        if(mReferencePath == null)
        {
            if(mContextPath.length() > 0)
            {
                SimpleStringBuffer buff = new SimpleStringBuffer(mContextPath);
                buff.append(PreferenceStore.PATH_SEPARATOR_STRING);
                buff.append(mName);
                mReferencePath = buff.toString();
            }
            else
            {
                mReferencePath = mName;
            }
        }
        return mReferencePath;
    }

    /**
     * Gets the name which identifies this <code>Preference</code> within its
     * context path.
     * @return the name which identifies this <code>Preference</code> within
     * its context path.
     */
    public String getName()
    {
        return mName;
    }

    /**
     * Gets the context path of this <code>Preference</code>.
     * @return the context path of this <code>Preference</code>
     */
    public String getContextPath()
    {
        return mContextPath;
    }

    /**
     * Gets the preference type.
     * @return the preference type. Should be a String name which follows the
     * conventions for naming Java packages,
     * e.g. <code>"oracle.portal.provider.v2.user"</code>.
     */
    public String getType()
    {
        return mType;
    }

    /**
     * Gets the full class name of the {@link PreferenceDataObject} stored
     * against this preference.
     * @return the full class name of the {@link PreferenceDataObject} stored
     * against this preference, if known, or <code>null</code> otherwise.
     */
    public String getDataClassName()
    {
        return mDataClassName;
    }

    /**
     * Gets the <code>Class</code> object representing the class of the {@link
     * PreferenceDataObject} stored against this preference.
     * @return <code>Class</code> object representing the class of the {@link
     * PreferenceDataObject} stored against this preference, if known, or
     * <code>null</code> otherwise.
     * @exception PreferenceStoreException if the class is unknown, or cannot
     * be loaded.
     */
    public Class getDataClass()
        throws PreferenceStoreException
    {
        if(mDataClass == null)
        {
            if(mDataClassName == null)
            {
                throw new PreferenceStoreException(null,
                                                   "Preference data class undefined");
            }
            else
            {
                SimpleStringBuffer exc = new SimpleStringBuffer(200);
                exc.append("Unable to find class \"");
                exc.append(mDataClassName);
                exc.append("\"");
                throw new PreferenceStoreException(null, exc.toString());
            }
        }
        return mDataClass;
    }

    /**
     * Get the data object associated with this <code>Preference</code>. The
     * <code>Preference</code>'s data object must have been supplied during
     * construction or loaded using {@link PreferenceStore#read} for this
     * method to return a non-null value.
     * @return the data object associated with this <code>Preference</code>, or
     * <code>null</code> if one hasn't yet been loaded.
     */
    public PreferenceDataObject getData()
    {
        return mDataObject;
    }

    /**
     * Sets the full class name of the {@link PreferenceDataObject} to be
     * loaded into this <code>Preference</code>. Note that if a data object is
     * currently held in this <code>Preference</code>, this will remove it as a
     * side effect. Note also that if the named class cannot be loaded, this will
     * not cause an exception, as it will still be possible to read the data as
     * an input stream using {@link PreferenceStore#getInputStream}.
     * @param dataClassName the full class name of the {@link
     * PreferenceDataObject} to be loaded into this <code>Preference</code>.
     * @exception PreferenceStoreException if the class is not a subclass of
     * {@link PreferenceDataObject}.
     */
    public void setDataClass(String dataClassName)
        throws PreferenceStoreException
    {
        if(dataClassName != null)
        {
            try
            {
                // We have to use the the current thread's 'context class loader'
                // (i.e. the web app's class loader) to load the class, so that it
                // can be picked up from within the web app
                setDataClass(Class.forName(dataClassName, true,
                                           Thread.currentThread().getContextClassLoader()));
            }
            catch(ClassNotFoundException e)
            {
                // Don't let not being able to get at the data class stop us
                // from streaming preference data
                mDataClass = null;
                mDataClassName = dataClassName;
                mDataObject = null;

                LogManager.log("Preference: Error loading data class \""
                               + dataClassName + "\"", e);

            }
        }
        else
        {
            mDataClass = null;
            mDataClassName = null;
        }
    }

    /**
     * Sets the <code>Class</code> object representing the class of the {@link
     * PreferenceDataObject} to be loaded into this
     * <code>Preference</code>. Note that if a data object is currently held in
     * this <code>Preference</code>, this will remove it as a side effect.
     * @param dataClassName the full class name of the {@link
     * PreferenceDataObject} to be loaded into this <code>Preference</code>.
     * @exception PreferenceStoreException if the class is not a subclass of
     * {@link PreferenceDataObject}.
     */
    public void setDataClass(Class dataClass)
        throws PreferenceStoreException
    {
        if(!PreferenceDataObject.class.isAssignableFrom(dataClass))
        {
            SimpleStringBuffer exc = new SimpleStringBuffer(200);
            exc.append("Preference data class \"");
            exc.append(dataClass.getName());
            exc.append("\"");
            exc.append(" does not implement \"");
            exc.append(PreferenceDataObject.class.getName());
            exc.append("\"");

            throw new PreferenceStoreException(null, exc.toString());

        }
        mDataClass = dataClass;
        mDataClassName = mDataClass.getName();

        // clear any existing data object
        mDataObject = null;
    }

    /**
     * Sets the data object to be stored under this <code>Preference</code>.
     * @param dataObject the data object to be stored under this
     * <code>Preference</code>.
     */
    public void setData(PreferenceDataObject dataObject)
        throws PreferenceStoreException
    {
        Class dataClass = dataObject.getClass();
        setDataClass(dataClass);
        mDataObject = dataObject;
    }


    // Initialization functions shared by constructor overloads
    private void initPath(String contextPath,
                          String name,
                          String type)
        throws PreferenceStoreException
    {
        // Validate the preference name here. The entire refernece path will be
        // validated when the subclass calls the encodePath() method
        if(!PreferenceStore.isValidPathComponent(name, true))
        {
            SimpleStringBuffer exc = new SimpleStringBuffer(200);
            exc.append("Invalid Preference name \"");
            exc.append(name);
            exc.append("\"");
            throw new PreferenceStoreException(null, exc.toString());
        }
        mName = name;
        mType = type;
        mContextPath = contextPath;

    }

    private void initPath(String referencePath,
                          String type)
        throws PreferenceStoreException
    {
        mReferencePath = referencePath;

        // Split the reference path up into its name and context parts
        int sepIndex = referencePath.lastIndexOf(PreferenceStore.PATH_SEPARATOR_CHAR);
        String name = "", contextPath = "";
        if(sepIndex != -1)
        {
            contextPath = referencePath.substring(0, sepIndex);
            if(++sepIndex < referencePath.length())
            {
                name = referencePath.substring(sepIndex);
            }
        }
        else
        {
            name = referencePath;
        }
        initPath(contextPath, name, type);
    }
}
