/* Copyright (c) Oracle Corporation 2001, All Rights Reserved. */
package oracle.portal.provider.v2.personalize;

import java.io.*;
import java.sql.*;
import java.util.*;
import javax.servlet.*;
import oracle.portal.log.LogManager;
import oracle.portal.provider.v2.*;
import oracle.portal.provider.v2.preference.*;
import oracle.portal.provider.v2.utils.ValidationError;
import oracle.portal.provider.v2.security.AccessControlException;
import oracle.portal.provider.v2.security.PortletSecurityManager;
import oracle.portal.utils.xml.v2.*;
import oracle.webdb.utils.SimpleStringBuffer;

/**
 * The <code>PrefStorePersonalizationManager</code> class implements a
 * <code>PortletPersonalizationManager</code> using a {@link PreferenceStore}
 * to persist customized data objects.<p>
 *
 * The class of the data objects managed by a
 * <code>PrefStorePersonalizationManager</code> is registered using {@link
 * #setDataClass(String)}. Most commonly this will be the result of a
 * declaration in the provider XML registry (e.g.
 * <code>&lt;dataClass><i>className</i>&lt;/dataClass></code>). For maximum
 * extensibility, it is recommended that you use {@link
 * NameValuePersonalizationObject} as a basis for your data class.<p>
 *
 */
public class PrefStorePersonalizationManager 
    extends PortletPersonalizationManager
    implements InitializableXMLObject
{
    protected PortletDefinition mPortletDefinition;
    protected PreferenceStore mPreferenceStore;

    // Lists of types to be operated on by the destroy() and copy()
    // operations.
    static final List sTypesToDestroy;
    static final List sTypesToCopy;

    static
    {
        // When a portlet or portlet instance is removed, we destroy all the
        // corresponding customization data
        sTypesToDestroy = new ArrayList(2);
        sTypesToDestroy.add(PreferenceStore.USER_CUSTOMIZATION_TYPE);
        sTypesToDestroy.add(PreferenceStore.DEFAULT_CUSTOMIZATION_TYPE);

        // We only copy over default customizations when a portlet is copied
        sTypesToCopy = new ArrayList(1);
        sTypesToCopy.add(PreferenceStore.DEFAULT_CUSTOMIZATION_TYPE);
    }
    
    /**
     * Null Constructor
     */
    public PrefStorePersonalizationManager() throws PersonalizationException
    {
    }

    public void preInitialize(Object parent) throws NodeHandlerException
    {
        if (!(parent instanceof PortletDefinition))
        {
            throw new NodeHandlerException("Parent of a PortletPersonalizationManager " +
                                           "must be a PortletDefinition");
        }
        mPortletDefinition = (PortletDefinition)parent;
    }

    public void postInitialize() throws NodeHandlerException
    {
        mPreferenceStore = mPortletDefinition.getPreferenceStore();
    }

    /**
     *  Initializes the repository for this
     *  <code>PrefStorePersonalizationManager</code>. 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 <code>PrefStorePersonalizationManager</code> currently does nothing
     *  in response to this call.
     *
     *  @param p the portlet this <code>PrefStorePersonalizationManager</code> is controlling.
     */
    public void init(PortletDefinition 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
     *  <code>PrefStorePersonalizationManager</code> uses {@link
     *  PreferenceStore#destroy(String,List)} to remove all the portlet
     *  customization data for this Portlet.
     *
     *  @param pd definition of the portlet this PortletPersonalizationManager
     *  is controlling.
     */
    public void destroy(ProviderInstance provider, PortletDefinition pd)
        throws AccessControlException, IOException
    {
        // don't do anything if the portlet doesn't support customizations
        if (!pd.hasShowEdit() && !pd.hasShowEditDefault())
            return;
        try
        {
            mPreferenceStore.destroy(getReferencePath(provider, pd), sTypesToDestroy);
        }
        catch(PreferenceStoreException pse)
        {
            throw new IOException(pse.getMessage());
        }
    }

    /**
     * Creates the initial personalization reference for this (new)
     * PortletReference and establishes its (static) defaults.<p>
     *
     * In response to this call, the
     * <code>PrefStorePersonalizationManager</code> creates a new instance of
     * the data object class that is registered to it, and calls its
     * <code>init()</code> method to initialize it with the appropriate
     * defaults. Finally, the default data is written to the {@link
     * PreferenceStore} to ensure it 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
        PersonalizationObject 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
     * <code>PrefStorePersonalizationManager</code> saves the passed object to
     * the {@link PreferenceStore}, 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 {@link PersonalizationObject}).
     * @param user the user attempting this operation.  The manager is expected
     * to authorize the user before completing this operation.
     */
    public void create(PortletReference ref, 
                       PersonalizationObject o, 
                       ProviderUser user)
        throws PortletAlreadyExistsException, 
               AccessControlException,
               IOException
    {
        try
        {
            Preference pref = getPreference(ref, o);
            mPreferenceStore.write(pref, false);
        }
        catch(EntryAlreadyExistsException eaee)
        {
            throw new PortletAlreadyExistsException(eaee.getMessage());
        }
        catch(PreferenceStoreException pse)
        {
            throw new IOException(pse.getMessage());
        }
    }

    /**
     *  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
     *  @param user the user making this request -- passed to give the manager access
     *  to the session.
     */
    public boolean exists(PortletReference ref, ProviderUser user)
        throws AccessControlException, IOException
    {
        Preference pref = null;
        try
        {
            pref = getPreference(ref);
            return mPreferenceStore.exists(pref);
        }
        catch(PreferenceStoreException pse)
        {
            SimpleStringBuffer msg = new SimpleStringBuffer(100);
            msg.append("PrefStorePersonalizationManager.exists(): Failed to ");
            msg.append("determine existence of: ");
            msg.append(pref.getReferencePath());
            
            LogManager.warning(msg.toString(), pse);
            throw new IOException(msg.toString());
        }
    }

    /**
     *  Returns a data object containing the customizations particular to this
     *  reference. If no customizations exist then the
     *  <code>PortletNotFoundException</code> 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
     */
    public PersonalizationObject read(PortletReference ref, ProviderUser user)
        throws AccessControlException, IOException
    {


        PersonalizationObject o = null;
        int type = ref.getType();
        Preference pref = null;

        try
        {
            do
            {
                try
                {
                    pref = getPreference(ref, null, type);
                    mPreferenceStore.read(pref);
                    o = (PersonalizationObject)pref.getData();
                    // If the portlet existed, we can exit the loop
                    break;
                }
                catch(EntryNotFoundException enfe)
                {
                    // If the portlet didn't exist, we carry on with the loop
                    SimpleStringBuffer msg = new SimpleStringBuffer(150);
                    msg.append("PrefStorePersonalizationManager: ");
                    msg.append("customization not found for reference: ");
                    msg.append(pref.getReferencePath());
                    msg.append(" Type: ");
                    msg.append(type);
                    msg.append(". Trying another type...");
            
                    LogManager.info(msg.toString());
                }                    
            }
            while(type-- >ref.TYPE_SYSTEM_DEFAULT);
        }
        catch(PreferenceStoreException pse)
        {
            SimpleStringBuffer msg = new SimpleStringBuffer(150);
            msg.append("PrefStorePersonalizationManager: Failed to read ");
            msg.append("customization for Portlet: ");
            msg.append(pref.getReferencePath());
            msg.append(" Type: ");
            msg.append(type);
            
            LogManager.severe(msg.toString(), pse);
            throw new IOException(msg.toString());
        }

        // Create a new default instance if we can't find one
        if(o == null)
        {
            SimpleStringBuffer msg = new SimpleStringBuffer(150);
            msg.append("PrefStorePersonalizationManager: Failed to locate ");
            msg.append("any customizations for Portlet: ");
            msg.append(pref.getReferencePath());
            msg.append(". Creating new default customization");
            LogManager.warning(msg.toString());

            pref = getPreference(ref, o, ref.TYPE_SYSTEM_DEFAULT);
            o = createDataObject(ref);
            try
            {
                pref.setData(o);
                mPreferenceStore.write(pref, true);
            }
            catch (PreferenceStoreException pse)
            {
                msg = new SimpleStringBuffer(150);
                msg.append("PrefStorePersonalizationManager: Failed to create ");
                msg.append("default customization for Portlet: ");
                msg.append(pref.getReferencePath());
            
                LogManager.severe(msg.toString(), pse);
                throw new IOException(msg.toString());
            }
        }
        return o;

    }

    /**
     *  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
     *  {@link PersonalizationObject}).
     *  @param user the user attempting this operation.  The manager is expected
     *  to authorize the user before completing this operation.
     */
    public void write(PortletReference ref, 
                      PersonalizationObject o, 
                      ProviderUser user)
        throws AccessControlException, IOException
    {
        Preference pref = null;
        try
        {
            pref = getPreference(ref, o);
            mPreferenceStore.write(pref, true);
        }
        catch(PreferenceStoreException pse)
        {
            SimpleStringBuffer msg = new SimpleStringBuffer(150);
            msg.append("PrefStorePersonalizationManager: Failed to write ");
            msg.append("customization for Portlet: ");
            msg.append(pref.getReferencePath());
            
            LogManager.severe(msg.toString(), pse);
            throw new IOException(msg.toString());
        }
    }

    /**
     *  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
    {
        try
        {
            if (ref.isDefault())
            {
                String instancePath = getReferencePath(ref.getPortletInstance());
                mPreferenceStore.destroy(instancePath, sTypesToDestroy);
            }
            else
            {
                Preference pref = getPreference(ref);
                mPreferenceStore.destroy(pref);
            }
        }
        catch(EntryNotFoundException e)
        {
            SimpleStringBuffer msg = new SimpleStringBuffer(150);
            msg.append("PrefStorePersonalizationManager: Failed to destroy ");
            msg.append("customizations for Portlet: ");
            msg.append(ref.toString());
            
            LogManager.severe(msg.toString(), e);
            throw new PortletNotFoundException(msg.toString());
        }
        catch(PreferenceStoreException pse)
        {
            SimpleStringBuffer msg = new SimpleStringBuffer(150);
            msg.append("PrefStorePersonalizationManager: Failed to destroy ");
            msg.append("customizations for portlet: ");
            msg.append(ref.toString());
            
            LogManager.severe(msg.toString(), pse);
            throw new IOException(msg.toString());
        }
    }

    /**
     *  Copies the instance customizations from one reference to another. If
     *  the reference refers to the system (default) instance the manager
     *  should additionally copy any other corresponding default customizations
     *  for different locales. 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
    {
        boolean isDefault = from.isDefault();

        // from and to should either be both default references or both
        // non-default references
        if(isDefault != to.isDefault())
        {
            SimpleStringBuffer msg = new SimpleStringBuffer(150);
            msg.append("PreStorePersonalizationManager: Failed to copy ");
            msg.append("customization. Source and destination references ");
            msg.append("are not of same type");
        
            LogManager.severe(msg.toString());
            throw new AccessControlException(msg.toString());
        }

        try
        {
            // If this is a 'default' reference, copy over all the
            // customizations of the 'default' type
            if(isDefault)
            {
                String fromContextPath = getReferencePath(from.getPortletInstance());
                String toContextPath = getReferencePath(to.getPortletInstance());
                mPreferenceStore.copy(fromContextPath,
                                      toContextPath,
                                      true,
                                      sTypesToCopy);
            }
            // Otherwise, copy over the customization for the indiviudal user
            else
            {
                Preference fromPref = getPreference(from);
                Preference toPref = getPreference(to);
                mPreferenceStore.copy(fromPref, toPref, true);
            }
        }
        catch(EntryNotFoundException enfe)
        {
            SimpleStringBuffer msg = new SimpleStringBuffer(150);
            msg.append("PrefStorePersonalizationManager: Failed to copy ");
            msg.append("customizations from: ");
            msg.append(from.toString());
            msg.append(" to: ");
            msg.append(to.toString());
            
            LogManager.severe(msg.toString(), enfe);
            throw new PortletNotFoundException(msg.toString());
        }
        catch(PreferenceStoreException pse)
        {
            SimpleStringBuffer msg = new SimpleStringBuffer(150);
            msg.append("PrefStorePersonalizationManager: Failed to copy ");
            msg.append("customizations for portlet: ");
            msg.append("customizations from: ");
            msg.append(from.toString());
            msg.append(" to: ");
            msg.append(to.toString());
            
            LogManager.severe(msg.toString(), pse);
            throw new IOException(msg.toString());
        }
    }

    /**
     * Validates the internal state of this
     * <code>PrefStorePersonalizationManager</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,
                                       "PrefStorePersonalizationManager",
                                       null);
        // Validate the supertype.
        super.validate(ctx, errors);

        // Validate members.
        if (mPortletDefinition == null)
        {
            errors.addError(ctx, "PotletDefintion not set.", null);
        }
        if (mPreferenceStore == null)
        {
            errors.addError(ctx, "PreferenceStore not set", null);
        }
    }

    protected Preference getPreference(PortletReference ref)
        throws IOException
    {
        return getPreference(ref, null, ref.getType());
    }

    protected Preference getPreference(PortletReference ref,
                                       PreferenceDataObject data)
        throws IOException
    {
        return getPreference(ref, data, ref.getType());
    }

    protected Preference getPreference(PortletReference ref,
                                       PreferenceDataObject data,
                                       int type)
        throws IOException
    {
        String preferenceName, preferenceType;
        switch(type)
        {
        case PortletReference.TYPE_SYSTEM_DEFAULT:
            preferenceName = "";
            preferenceType = PreferenceStore.DEFAULT_CUSTOMIZATION_TYPE;
            break;
        case PortletReference.TYPE_LOCALE_DEFAULT:
            preferenceName = getLanguagePath(ref.getLocale());
            preferenceType = PreferenceStore.DEFAULT_CUSTOMIZATION_TYPE;
            break;
        case PortletReference.TYPE_USER:
            preferenceName = ref.getUser().getName();
            preferenceType = PreferenceStore.USER_CUSTOMIZATION_TYPE;
            break;
        default:
            {
                SimpleStringBuffer msg = new SimpleStringBuffer(150);
                msg.append("PrefStorePersonalizationManager: Unknown ");
                msg.append("reference type: ");
                msg.append(type);

                IOException ioe = new IOException(msg.toString());
                LogManager.severe(msg.toString(), ioe);
                throw ioe;
            }
        }

        String refPath = getReferencePath(ref.getPortletInstance());
        try
        {
            if(data == null)
            {
                return new Preference(refPath,
                                      preferenceName,
                                      preferenceType,
                                      getDataClass().getName());
            }
            else
            {
                return new Preference(refPath,
                                      preferenceName,
                                      preferenceType,
                                      data);
            }
        }
        catch(PreferenceStoreException pse)
        {
            SimpleStringBuffer msg = new SimpleStringBuffer(150);
            msg.append("PrefStorePersonalizationManager: Failed to read");
            msg.append("preference for portlet: ");
            msg.append(refPath);

            LogManager.severe(msg.toString(), pse);
            throw new IOException(msg.toString());
        }        
    }


    // Derive the portlet prefix of the reference path key, with LEAST selective
    // values first
    private String getReferencePath(ProviderInstance provider, PortletDefinition p)
    {
        return provider.getProviderId() +
            PreferenceStore.PATH_SEPARATOR_CHAR +
            p.getId();
    }

    // Derive the portlet instance prefix of the reference path key, with LEAST
    // selective values first
    private String getReferencePath(PortletInstance inst)
    {
        return getReferencePath(inst.getProviderInstance(),
                                inst.getPortletDefinition()) +
            PreferenceStore.PATH_SEPARATOR_CHAR +
            inst.getInstanceName();
    }

    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 PersonalizationObject createDataObject(PortletReference ref)
        throws IOException
    {
        PersonalizationObject po = null;
        try
        {
            po = (PersonalizationObject)getDataClass().newInstance();
            po.init(ref);
        }
        catch(Exception e)
        {
            SimpleStringBuffer msg = new SimpleStringBuffer(150);
            msg.append("PrefStorePersonalizationManager: Failed to create ");
            msg.append("data object for reference: ");
            msg.append(ref.toString());
            msg.append(" Dataclass: ");
            msg.append(getDataClass().getName());

            LogManager.severe(msg.toString(), e);
            throw new IOException(msg.toString());
        }
        return po;
    }
}
