/* ====================================================================
 * The Apache Software License, Version 1.1
 *
 * Copyright (c) 2000-2001 The Apache Software Foundation.  All rights
 * reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution,
 *    if any, must include the following acknowledgment:
 *       "This product includes software developed by the
 *        Apache Software Foundation (http://www.apache.org/)."
 *    Alternately, this acknowledgment may appear in the software itself,
 *    if and wherever such third-party acknowledgments normally appear.
 *
 * 4. The names "Apache" and "Apache Software Foundation" and
 *     "Apache Jetspeed" must not be used to endorse or promote products
 *    derived from this software without prior written permission. For
 *    written permission, please contact apache@apache.org.
 *
 * 5. Products derived from this software may not be called "Apache" or
 *    "Apache Jetspeed", nor may "Apache" appear in their name, without
 *    prior written permission of the Apache Software Foundation.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 */

package org.apache.jetspeed.services.profiler;

//java.util
import java.util.Map;
import java.util.HashMap;
import java.util.Enumeration;
import java.util.Locale;
import java.util.Iterator;
import java.util.List;
import java.util.LinkedList;
import java.util.Vector;

// java.net
import javax.servlet.ServletConfig;

//turbine.util
import org.apache.turbine.util.Log;
import org.apache.turbine.util.RunData;
import org.apache.turbine.util.DynamicURI;

// turbine.services
import org.apache.turbine.services.TurbineBaseService;
import org.apache.turbine.services.TurbineServices;
import org.apache.turbine.services.InitializationException;
import org.apache.turbine.services.localization.LocaleDetector;
import org.apache.turbine.services.resources.TurbineResources;
import org.apache.turbine.services.resources.ResourceService;

// jetspeed.om
import org.apache.jetspeed.om.security.JetspeedUser;
import org.apache.jetspeed.om.security.Role;
import org.apache.jetspeed.om.security.Group;

import org.apache.jetspeed.om.profile.Profile;
import org.apache.jetspeed.om.profile.Portlets;
import org.apache.jetspeed.om.profile.QueryLocator;
import org.apache.jetspeed.om.profile.ProfileLocator;
import org.apache.jetspeed.om.profile.BaseProfileLocator;
import org.apache.jetspeed.om.profile.BaseProfile;
import org.apache.jetspeed.om.profile.ProfileException;
import org.apache.jetspeed.om.profile.psml.PsmlPortlets;
import org.apache.jetspeed.om.profile.PSMLDocument;
import org.apache.jetspeed.om.profile.psml.PsmlControl;
import org.apache.jetspeed.om.profile.psml.PsmlController;
import org.apache.jetspeed.om.profile.Control;
import org.apache.jetspeed.om.profile.Controller;
import org.apache.jetspeed.om.profile.Skin;
import org.apache.jetspeed.om.profile.psml.PsmlSkin;
import org.apache.jetspeed.om.profile.BasePSMLDocument;
import org.apache.jetspeed.om.registry.MediaTypeRegistry;
import org.apache.jetspeed.om.registry.MediaTypeEntry;
import org.apache.jetspeed.om.profile.Entry;

// jetspeed.capability
import org.apache.jetspeed.capability.CapabilityMap;
import org.apache.jetspeed.capability.CapabilityMapFactory;

// jetspeed.services
import org.apache.jetspeed.services.Profiler;
import org.apache.jetspeed.services.rundata.JetspeedRunData;
import org.apache.jetspeed.services.Registry;
import org.apache.jetspeed.services.JetspeedSecurity;
import org.apache.jetspeed.services.PsmlManager;
import org.apache.jetspeed.services.idgenerator.JetspeedIdGenerator;

// jetspeed.util
import org.apache.jetspeed.util.MimeType;

/**
 * <p>This is an implementation of the <code>Profiler</code> interface.
 *
 * This implementation maps requests to profiles (PSML resources) based on
 * request parameters, requesting deviced capabilities, and the device's
 * language. </p>
 * <p>This service expects these properties to be set for correct operation:
 * <dl>
 *    <dt>root</dt><dd>The webapp rel. path to the root profiling directory</dd>
 *    <dt>resource.default</dt><dd>The default resource filename</dd>
 *    <dt>resource.ext</dt><dd>The default resource filename extension</dd>
 *    <dt>security</dt><dd>Use security flag</dd>
 *    <dt>fallback.language</dt><dd>Use language configuration flag</dd>
 *    <dt>fallback.country</dt><dd>Use country configuration flag</dd>
 *    <dt>fallback.to.root</dt><dd>Continue falling back past media type flag</dd>
 *
 * </dl>
 * </p>
 *
 * @author <a href="mailto:david@bluesunrise.com">David Sean Taylor</a>
 * @author <a href="mailto:sgala@hisitech.com">Santiago Gala</a>
 * @author <a href="mailto:morciuch@apache.org">Mark Orciuch</a>
 * @version $Id: JetspeedProfilerService.java,v 1.43 2002/12/12 17:14:47 morciuch Exp $
 */

public class JetspeedProfilerService  extends TurbineBaseService
    implements ProfilerService
{

    // configuration keys
    private final static String CONFIG_RESOURCE_DEFAULT = "resource.default";
    private final static String CONFIG_RESOURCE_EXT     = "resource.ext";
    private final static String CONFIG_SECURITY         = "security";
    private final static String CONFIG_ROLE_FALLBACK    = "rolefallback";
    private final static String CONFIG_NEWUSER_TEMPLATE  = "newuser.template";
    private final static String CONFIG_NEWUSER_MEDIA     = "newuser.media_types";
    private final static String CONFIG_FALLBACK_LANGUAGE = "fallback.language";
    private final static String CONFIG_FALLBACK_COUNTRY = "fallback.country";
    private final static String CONFIG_FALLBACK_TO_ROOT = "fallback.to.root";
    private final static String CONFIG_ROLE_MERGE = "rolemerge";
    private final static String CONFIG_ROLE_MERGE_CONTROL = "rolemerge.control";
    private final static String CONFIG_ROLE_MERGE_CONTROLLER = "rolemerge.controller";

    // default configuration values
    private final static String DEFAULT_CONFIG_RESOURCE_DEFAULT = "default";
    private final static String DEFAULT_CONFIG_RESOURCE_EXT = ".psml";
    private final static boolean DEFAULT_CONFIG_SECURITY = false;
    private final static boolean DEFAULT_CONFIG_ROLE_FALLBACK = true;
    private final static String DEFAULT_CONFIG_NEWUSER_TEMPLATE = null;
    private final static String [] DEFAULT_CONFIG_NEWUSER_MEDIA =
    { "html", "wml" };
    private final static String DEFAULT_CONFIG_ROLE_MERGE_CONTROL = "TabControl";
    private final static String DEFAULT_CONFIG_ROLE_MERGE_CONTROLLER = "TabController";

    private final static String PATH_EXTENSION_DELIMITER = ".";
    // messages
    private final static String MSG_MISSING_PARAMETER =
        "JetspeedProfilerService initialization failed. Missing parameter:";


    // configuration parameters
    String root;                   // the root psml resource directory
    String resourceDefault;        // the default name for a resource
    String resourceExt;            // the default extension for a resource
    String rolemergeControl;       // the default control used with merged role profiles
    String rolemergeController;    // the default controller used with merged role profiles

    // MODIFIED: A. Kempf
    String newUserTemplate = DEFAULT_CONFIG_NEWUSER_TEMPLATE;

    boolean useSecurity = false;   // use security features
    boolean useRoleFallback = true;
    boolean useFallbackLanguage = true;
    boolean useFallbackCountry = true;
    boolean useFallbackToRoot = false;
    boolean useRoleMerge = false;

    String mediaTypes[] = null;

    /**
     * This methode creates a wml profile and a html profile
     * for a new user
     * --------------------------------------------------------------------------
     * last modified: 10/31/01
     * Andreas Kempf, Siemens ICM S CP PE, Munich
     * mailto: A.Kempf@web.de
     */
    public Profile createProfile(RunData data, Profile profile)
            throws ProfileException
    {
        Profile current = null;
        CapabilityMap map;

        if (data == null)
        {
            map = CapabilityMapFactory.getDefaultCapabilityMap();
        }
        else
        {
            map = ((JetspeedRunData)data).getCapability();
        }
        String mediaType = getMediaType(data, map);

        // if template is null then use role-based psml
        if (newUserTemplate == null)
            return current;

        if (mediaTypes != null)
        {
            Profile dummy;
            for (int ix=0; ix < mediaTypes.length; ix++)
            {
                dummy = createProfile(data, profile, mediaTypes[ix], newUserTemplate);
                if (mediaTypes[ix].equalsIgnoreCase(mediaType))
                    current = dummy;
            }
        }
        return current;
    }

    // --------------------------------------------------------------------------

    /**
     * This is the early initialization method called by the
     * Turbine <code>Service</code> framework
     * @param conf The <code>ServletConfig</code>
     * @exception throws a <code>InitializationException</code> if the service
     * fails to initialize
     */
    public synchronized void init(ServletConfig conf) throws InitializationException
    {

        // already initialized
        if (getInit()) return;

        try
        {
            initConfiguration();
        }
        catch (Exception e)
        {
            Log.error("Profiler: Failed to load Service: " + e);
            e.printStackTrace();
        }

        // initialization done
        setInit(true);

     }

    /**
     * This is the shutdown method called by the
     * Turbine <code>Service</code> framework
     */
    public void shutdown()
    {
    }

    /**
     *  get the Profile object using the Rundata state and capability map
     *  this is the mapping functionality of the profiler
     *
     * @param rundata the rundata object for the current request
     * @param cm the <code>CapabilityMap</code> of the current requesting device
     * @return a new Profile object
     */
    public Profile getProfile(RunData data, CapabilityMap cm)
        throws ProfileException
    {
        JetspeedRunData rundata = (JetspeedRunData)data;
        Profile profile = fallbackProfile(rundata, cm);
        if (null == profile && useRoleFallback)
        {
            Vector profiles = new Vector();
            JetspeedUser user = rundata.getJetspeedUser();
            if (user != null)
            {
                try
                {
                    Iterator roles = JetspeedSecurity.getRoles(user.getUserName());
                    if (roles != null)
                    {
                        while (roles.hasNext())
                        {
                            // note: this is an unordered list. will need to change db schema to order it
                            Role role = (Role)roles.next();
                            rundata.getParameters().setString( Profiler.PARAM_ROLE, role.getName() );
                            profile = fallbackProfile(rundata, cm);
                            if (profile != null)
                            {
                                profiles.add(profile);
                            }
                        }
                        profile = mergeRoleProfiles(data, profiles);

                        // If something went wrong with merging, attempt another fallback
                        if (profile == null)
                        {
                            profile = fallbackProfile(rundata, cm);
                        }
                    }
                }
                catch (Exception e)
                {
                    throw new ProfileException(e.toString());
                }
            }
        }
        return profile;
    }

    /**
     * Merge role profiles to create default profile. Resulting psml will be a set of
     * tabs. If role's psml is a tab control, each tab is placed in the resulting psml
     * as is. Otherwise, a new tab is created and psml is placed there. In this case,
     * tab name will be derived from role's name. For example, if role name is "news",
     * the resulting profile name will be "News Home".
     *
     * @param data
     * @param profiles Vector of profiles for all roles user is part of
     * @return Merged profile
     * @exception Exception
     */
    private Profile mergeRoleProfiles(RunData data, Vector profiles)
        throws Exception
    {
        Profile result = null;
        // If merge feature is not turned on, return
        // profile for the first role (if any)
        if (!this.useRoleMerge)
        {
            if (profiles.size() > 0)
            {
                result = (Profile) profiles.get(0);
            }
        }
        // Proceed with merging all profiles
        else if (profiles.size() > 0)
        {
            try
            {
                // Create an empty portlet container
                Portlets portlets = new PsmlPortlets();
                Control control = new PsmlControl();
                control.setName(this.rolemergeControl);
                portlets.setControl(control);
                Controller controller = new PsmlController();
                controller.setName(this.rolemergeController);
                portlets.setController(controller);

                // Set the skin
                Skin skin = new PsmlSkin();
                skin.setName(org.apache.jetspeed.services.PortalToolkit.getSkin((String) null).getName());
                portlets.setSkin(skin);

                String mediaType = null;

                // Process each role profile
                for (Iterator it = profiles.iterator(); it.hasNext(); )
                {
                    Profile roleProfile = (Profile)it.next();
                    mediaType = mediaType == null ? roleProfile.getMediaType() : mediaType;
                    Profile tmpProfile = (Profile) roleProfile.clone();
                    Portlets tmpPortlets = tmpProfile.getDocument().getPortlets();

                    // If topmost control is a tab control, then add each tab to the container
                    Control tabControl = tmpPortlets.getControl();
                    if (tabControl != null && tabControl.getName().equals(this.rolemergeControl))
                    {
                        int tabCount = tmpPortlets.getPortletsCount();
                        for (int i = 0; i < tabCount; i++)
                        {
                            Portlets tab = tmpPortlets.getPortlets(i);
                            portlets.addPortlets(tab);
                        }
                    }
                    // Otherwise, add the contents of profile as a tab
                    else
                    {
                        String title = org.apache.turbine.util.StringUtils.firstLetterCaps(roleProfile.getRoleName());
                        tmpPortlets.setTitle(title + " Home");
                        portlets.addPortlets(tmpPortlets);
                    }

                    if (Log.getLogger().isDebugEnabled())
                    {
                        Log.debug("Profiler: Processing profile for role " + roleProfile.getRoleName());
                    }
                }

                // Create a new profile for the user
                ProfileLocator locator = createLocator();
                locator.setUser((JetspeedUser) data.getUser());
                locator.setMediaType(mediaType);
                locator.setName(this.resourceDefault + this.resourceExt);

                // Regenerate the portlet ids so they are unique
                this.regenerateIds(portlets);

                // Save the new profile to permament storage
                result = this.createProfile(locator, portlets);

            }
            catch (Exception e) {
                Log.error(e);
            }
        }

        return result;
    }

    /**
     * Regenerates jspeid for all portlets and entries
     *
     * @param profile Profile to process
     * @return Profile with portlet ids regenerated
     */
    private void regenerateIds(Portlets topPortlets)
    throws Exception
    {
        // Display some portlets
        Portlets[] portlets = topPortlets.getPortletsArray();
        for (int i = 0; i < portlets.length; i++)
        {
            portlets[i].setId(JetspeedIdGenerator.getNextPeid());
            Entry[] entries = portlets[i].getEntriesArray();
            for (int j = 0; j < entries.length; j++)
            {
                entries[j].setId(JetspeedIdGenerator.getNextPeid());
            }
            regenerateIds(portlets[i]);
        }
    }

    /**
     *  get the Profile object using the Rundata state and capability map
     *  this is the mapping functionality of the profiler
     *
     * @param rundata the rundata object for the current request
     * @param cm the <code>CapabilityMap</code> of the current requesting device
     * @return a new Profile object
     */
    protected Profile fallbackProfile(RunData data, CapabilityMap cm)
        throws ProfileException
    {
        try
        {
            JetspeedRunData rundata = (JetspeedRunData)data;
            Profile profile = createProfile();
            JetspeedUser user = rundata.getJetspeedUser();

            // get the media type from the capability map or rundata
            profile.setMediaType(getMediaType(rundata, cm));

            //  Is it a group, role, or user resource?
            //  It can only be one
            String param = rundata.getParameters().getString( Profiler.PARAM_GROUP );

            if (null != param)
            {
                // GROUP Resource
                profile.setGroup( JetspeedSecurity.getGroup(param) );
            }
            else
            {
                param = rundata.getParameters().getString( Profiler.PARAM_ROLE );
                if (null != param)
                {
                    // ROLE Resource
                    if (user.hasLoggedIn())  // disallow role access for anonymous user
                    {
                        profile.setRole( JetspeedSecurity.getRole(param) );
                    }
                    else
                    {
                        profile.setAnonymous(true);
                        profile.setUser( user );
                    }
                }
                else  // it must be a user resource or anonymous resource
                {
                    // accessing another user's resource
                    param = rundata.getParameters().getString( Profiler.PARAM_USER );
                    if (null != param)
                    {

                        if (param.equals(JetspeedSecurity.getAnonymousUserName()))
                        {
                            profile.setAnonymous(true);
                        }
                        if (user.getUserName().equals(param))
                        {
                            profile.setUser( user );
                        }
                        else
                        {
                            profile.setUser( JetspeedSecurity.getUser(param) );
                        }
                    }
                    else
                    {
                        profile.setAnonymous(user.getUserName().equals(JetspeedSecurity.getAnonymousUserName()));
                        profile.setUser( user );
                    }
                }
            }

            // get resource name
            StringBuffer resource = new StringBuffer();
            param = rundata.getParameters().getString( Profiler.PARAM_PAGE );
            if (null == param)
            {
               // the default resource
                resource.append( resourceDefault );
                resource.append( resourceExt );
            }
            else
            {   // a specific resource
                resource.append( param );
                if ( -1 == param.indexOf( PATH_EXTENSION_DELIMITER ) )
                    resource.append( resourceExt );
            }
            profile.setName( resource.toString() );

            // LANGUAGE
            getLanguageSettings(profile, rundata);

            PSMLDocument doc = fallback( profile );
            if (null != doc)
            {
                profile.setDocument( doc );
                return profile;
            }
        }
        catch (Exception e)
        {
            throw new ProfileException(e.toString());
        }
        return null;
    }

    /**
     *  get the Profile object using the Rundata state and capability map
     *  this is the mapping functionality of the profiler
     *
     * @param rundata the rundata object for the current request
     * @return a new Profile object
     */
    public Profile getProfile(RunData rundata)
        throws ProfileException
    {
        CapabilityMap cm = null;

        if (rundata instanceof JetspeedRunData)
        {
            cm = ((JetspeedRunData)rundata).getCapability();
        }
        else
        {
            cm = CapabilityMapFactory.getCapabilityMap( rundata );
        }

        return getProfile(rundata, cm);
    }

    /**
     *  get the Profile object using the Rundata state and specific mimetype
     *
     * @deprecated Do not use a profiler method based on MimeType
     *
     * @param rundata the rundata object for the current request
     * @param mt the <code>MimeType</code> of the current requesting device
     * @return a new Profile object
     */
    public Profile getProfile(RunData data, MimeType mt)
        throws ProfileException
    {
        CapabilityMap cm = CapabilityMapFactory.getCapabilityMap(mt.toString());
        return getProfile(data, cm);
    }

    /**
     *  get the Profile object using a profile locator
     *
     * @param rundata The rundata object for the current request.
     * @param locator The locator containing criteria describing the profile.
     * @return a new Profile object
     */
    public Profile getProfile(ProfileLocator locator)
        throws ProfileException
    {
        PSMLDocument doc =  fallback(locator);
        Profile profile = createProfile(locator);
        profile.setDocument(doc);
        return profile;
    }

    /*
     * Gets the language and country parameters from the request using the Turbine locale detector.
     *
     * @param profile The profile object which is modified with the new language settings.
     * @param rundata The request specific state.
     */
    protected void getLanguageSettings( Profile profile, RunData rundata )
    {
        String language = rundata.getParameters().getString(Profiler.PARAM_LANGUAGE);

        if (language != null)
        {
            // dont use locale based fall back
            if (language.equals("-1"))
            {
                return;
            }
            else
            {
                profile.setLanguage(language);
            }

            String country = rundata.getParameters().getString(Profiler.PARAM_COUNTRY);
            if (country != null)
            {
                profile.setCountry(country);
            }
        }
        else
        {
            Locale locale = (Locale)rundata.getUser().getTemp("locale");
            if (locale == null)
            {
                // Get the locale store it in the user object
                locale = LocaleDetector.getLocale(rundata);
                if (locale == null)
                {
                    locale = new Locale(
                                    TurbineResources.getString("locale.default.language", "en"),
                                    TurbineResources.getString("locale.default.country", "US"));
                }
                rundata.getUser().setTemp("locale", locale);
            }

            if (useFallbackLanguage)
            {
                profile.setLanguage( locale.getLanguage() );
            }

            if (useFallbackCountry)
            {
                profile.setCountry( locale.getCountry() );
            }
        }
    }

    /*
     * A basic profiler fallback algorithm that starts from the most specific parameters,
     * going to the least specific parameters. The PsmlManager implementation is passed
     * a list of ProfileLocators ordered from most specific  to least specific.
     *
     * @param locator The profile locator criteria used to locate a profile.
     * @param rundata The request specific state.
     * @return The found psml document, or null if not found.
     */
    protected PSMLDocument fallbackList( ProfileLocator original, RunData rundata )
    {
        try
        {
            List locators = new LinkedList();
            ProfileLocator locator = (ProfileLocator)original.clone();

            locators.add( locator.clone() );

            // remove country
            if (null != original.getCountry())
            {
                locator.setCountry(null);
                locators.add( locator.clone() );
            }

            // remove language
            if (null != original.getLanguage())
            {
                locator.setLanguage(null);
                locators.add( locator.clone() );
            }

            // fallback mediaType
            if (null != original.getMediaType())
            {
                locator.setMediaType(null);
                locators.add( locator.clone() );
            }

            if (null != original.getGroup())
            {
                locator.setGroup(null);
                locators.add( locator.clone() );
            }
            else if (null != original.getRole())
            {
                locator.setRole(null);
                locators.add( locator.clone() );
            }
            else if (null != original.getUser())
            {
                locator.setUser(null);
                locators.add( locator.clone() );
            }
            PSMLDocument doc = PsmlManager.getDocument( locators );
            return doc;

        }
        catch (CloneNotSupportedException e)
        {
            Log.error("Profiler: Could not clone profile locator object", e);
        }
        return null;
    }

    /*
     * A basic profiler fallback algorithm that starts from the most specific parameters,
     * going to the least specific parameters. The PsmlManager implementation is passed
     * a list of ProfileLocators ordered from most specific  to least specific.
     *
     * This is alternate fallback algorithm.
     *
     * @param locator The profile locator criteria used to locate a profile.
     *
     * @return PSMLDocument The located document or null.
     */
    protected PSMLDocument fallback(ProfileLocator locator)
    {
        if (Log.getLogger().isDebugEnabled())
        {
            Log.debug( "Profiler: fallback called with: " + locator );
        }

        PSMLDocument doc = PsmlManager.getDocument( locator );
        if (null != doc)
            return doc;

        // remove country
        if (null != locator.getCountry())
        {
            locator.setCountry(null);
            doc = PsmlManager.getDocument( locator );
            if (null != doc)
                return doc;
        }

        // remove language
        if (null != locator.getLanguage())
        {
            locator.setLanguage(null);
            doc = PsmlManager.getDocument( locator );
            if (null != doc)
                return doc;
        }

        // fallback mediaType
        if (useFallbackToRoot)
        {
            if (null != locator.getMediaType())
            {
                locator.setMediaType(null);
                doc = PsmlManager.getDocument( locator );
                if (null != doc)
                    return doc;
            }
        }

        if (!useRoleFallback)
        {
            if (null != locator.getGroup())
            {
                locator.setGroup(null);
                doc = PsmlManager.getDocument( locator );
                if (null != doc)
                    return doc;
            }
            else if (null != locator.getRole())
            {
                locator.setRole(null);
                doc = PsmlManager.getDocument( locator );
                if (null != doc)
                    return doc;
            }
            else if (null != locator.getUser())
            {
                locator.setUser(null);
                doc = PsmlManager.getDocument( locator );
                if (null != doc)
                    return doc;
            }
        }
        return doc;

    }

    /**
     * Lookup the media type from the CapabilitMap.
     * First the RunData is checked for an explicit media-type request.
     *
     * @param cm The <code>CapabilityMap</code> of the current requesting device.
     * @param rundata, The <code>RunData</code> turbine request context information.
     * @return a String, the unique name of the media type.
     */
    protected String getMediaType(RunData rundata, CapabilityMap cm)
    {
        String paramMediaType;
        String media = null;

        if (null != rundata)
        {
            paramMediaType = rundata.getParameters().getString( Profiler.PARAM_MEDIA_TYPE );
            if (null != paramMediaType)
            {
                return paramMediaType;
            }
        }

        if (cm != null)
        {
            media = cm.getPreferredMediaType();
        }

        return media;
    }

    /**
     * Loads the configuration parameters for this service from the
     * JetspeedResources.properties file.
     *
     * @exception throws a <code>InitializationException</code> if the service
     * fails to initialize
     */
    private void initConfiguration() throws InitializationException
    {
        // get configuration parameters from Jetspeed Resources
        ResourceService serviceConf = ((TurbineServices)TurbineServices.getInstance())
                                                     .getResources(ProfilerService.SERVICE_NAME);

        resourceDefault = serviceConf.getString( CONFIG_RESOURCE_DEFAULT, DEFAULT_CONFIG_RESOURCE_DEFAULT );

        resourceExt = serviceConf.getString( CONFIG_RESOURCE_EXT, DEFAULT_CONFIG_RESOURCE_EXT );
        if (-1 == resourceExt.indexOf(PATH_EXTENSION_DELIMITER))
            resourceExt = PATH_EXTENSION_DELIMITER + resourceExt;

        useSecurity = serviceConf.getBoolean( CONFIG_SECURITY, DEFAULT_CONFIG_SECURITY );

        useRoleFallback = serviceConf.getBoolean( CONFIG_ROLE_FALLBACK, DEFAULT_CONFIG_ROLE_FALLBACK );

        newUserTemplate = serviceConf.getString( CONFIG_NEWUSER_TEMPLATE, DEFAULT_CONFIG_NEWUSER_TEMPLATE );

        useFallbackToRoot = serviceConf.getBoolean( CONFIG_FALLBACK_TO_ROOT, useFallbackToRoot );

        useFallbackLanguage = serviceConf.getBoolean( CONFIG_FALLBACK_LANGUAGE, useFallbackLanguage );

        useRoleMerge = serviceConf.getBoolean( CONFIG_ROLE_MERGE, useRoleMerge );

        rolemergeControl = serviceConf.getString( CONFIG_ROLE_MERGE_CONTROL, DEFAULT_CONFIG_ROLE_MERGE_CONTROL );

        rolemergeController = serviceConf.getString( CONFIG_ROLE_MERGE_CONTROLLER, DEFAULT_CONFIG_ROLE_MERGE_CONTROLLER );

        if (useFallbackLanguage == false)
        {
            useFallbackCountry = false;
        }
        else
        {
            useFallbackCountry = serviceConf.getBoolean( CONFIG_FALLBACK_COUNTRY, useFallbackCountry );
        }

        try
        {
            mediaTypes = serviceConf.getStringArray(CONFIG_NEWUSER_MEDIA);
        }
        catch (Exception e)
        {}

        if (null == mediaTypes || mediaTypes.length == 0)
        {
            mediaTypes = DEFAULT_CONFIG_NEWUSER_MEDIA;
        }
    }

   /**
    * Builds a dynamic URI based on the current profiler group/role/page
    *
    * @param data The rundata object for the current request.
    * @param locator The description of the profile.
    * @return A new dynamic URI representing all profile parameters from the locator.
    */
    public DynamicURI makeDynamicURI( RunData data, ProfileLocator locator )
        throws ProfileException
    {
        DynamicURI uri = new DynamicURI( data );

       // check mediatype to add to the uri
        String mtype = locator.getMediaType();
        if (null != mtype)
        {
            uri.addPathInfo(Profiler.PARAM_MEDIA_TYPE, mtype);
        }

       // check language to add to the uri
        String language = locator.getLanguage();
        if (null != language)
        {
            uri.addPathInfo(Profiler.PARAM_LANGUAGE, language);
        }

       // check language to add to the uri
        String country = locator.getCountry();
        if (null != country)
        {
            uri.addPathInfo(Profiler.PARAM_COUNTRY, country);
        }

        // check User, Group or Role to add to the uri
        JetspeedUser user = locator.getUser();
        if (null != user)
        {
            if (user.getUserName() != null)
                uri.addPathInfo(Profiler.PARAM_USER, user.getUserName());
        }
        else
        {
            Group group = locator.getGroup();
            if (null != group)
            {
                uri.addPathInfo(Profiler.PARAM_GROUP, group.getName());
            }
            else
            {
                Role role = locator.getRole();
                if (null != role)
                {
                    uri.addPathInfo(Profiler.PARAM_ROLE, role.getName());
                }
            }
        }

        // check Page to add to the uri
        String page = locator.getName();
        if (null != page)
        {
            uri.addPathInfo(Profiler.PARAM_PAGE, page);
        }

        return uri;
    }

    /**
     * Creates a new Profile object that can be successfully managed by
     * the current Profiler implementation
     *
     * @return A new Profile object
     */
    public Profile createProfile()
    {
        return new BaseProfile();
    }

    /**
     * Creates a new Profile object for a specific locator.
     *
     * @param locator The description of the profile.
     * @return A new Profile object
     */
    public Profile createProfile(ProfileLocator locator)
    {
        return new BaseProfile(locator);
    }

    /**
     * Creates a new ProfileLocator object that can be successfully managed by
     * the current Profiler implementation
     *
     * @return A new ProfileLocator object
     */
    public ProfileLocator createLocator()
    {
        return new BaseProfileLocator();
    }

    /**
     * Create a new profile given a profile locator
     *
     * @param locator The description of the new profile to be created.
     * @param portlets The PSML tree
     */

    public Profile createProfile(ProfileLocator locator, Portlets portlets)
            throws ProfileException
    {
        if (portlets == null)
        {
            portlets = new PsmlPortlets();
        }
        Profile profile = createProfile(locator);
        PSMLDocument doc = new BasePSMLDocument(null, portlets);
        profile.setDocument(doc);
        doc = PsmlManager.createDocument(profile);
        profile.setDocument(doc);
        return profile;
    }

    /**
     * Create a new profile.
     * The profile parameter's document will be cloned.
     *
     * @param rundata The rundata object for the current request.
     * @param profile The description of the new profile to be created.
     * @param contentType create a profile for the specific contentType
     * @param from create a profile by cloning the profile from the specific user (if null - turbine is used)
     * @return The newly created profile.
     * -----------------------------------------------------------
     * Andreas Kempf, Siemens ICM S CP PE, Munich
     */

    /**
     * This methode creates a wml profile and a html profile
     * for a new user
     */

    public Profile createProfile( RunData data, Profile profile, String contentType, String from )
            throws ProfileException
    {
      if ((contentType == null) || (contentType.length() < 2))
        contentType = "html";

      if ((from == null) || (from.length() < 2))
        from = "turbine";


        if ((null == profile.getDocument()) || (!profile.getMediaType().equalsIgnoreCase (contentType)))
        {
            // locate the default resource

            // TODO: make this configurable

            try
            {
                ProfileLocator locator = createLocator();
                locator.setUser( JetspeedSecurity.getUser(from) );

                locator.setMediaType(contentType);
                PSMLDocument doc = fallback(locator);

                if (doc != null)
                    profile.setDocument(doc);

                profile.setName( resourceDefault + resourceExt );

            }
            catch (Exception e)
            {
                throw new ProfileException(e.toString());
            }
        }

        try
        {
            profile.setMediaType(contentType);

            PSMLDocument doc = PsmlManager.createDocument(profile);
            Profile newProfile = (Profile)profile.clone();
            newProfile.setDocument(doc);

            return newProfile;
        }
        catch (CloneNotSupportedException e)
        {
            Log.error("Could not clone profile locator object: ", e);
        }
        return null;

    }

   /** Create a new profile.
     *
     * @deprecated Should be removed when old customizer is removed.
     *
     * @param rundata The rundata object for the current request.
     * @param profile The description of the new profile to be created.
     * @param mt The specific mime type, which is converted to a mediatype.
     * @return The newly created profile.
     */
    public Profile createProfile( RunData data, Profile profile, MimeType mt )
        throws ProfileException
    {
        CapabilityMap cm = CapabilityMapFactory.getCapabilityMap(mt.getContentType());
        profile.setMediaType( getMediaType(data, cm) );
        return createProfile(data, profile);
    }

   /**
     *  Removes a profile.
     *
     * @param locator The profile locator criteria.
     */
    public void removeProfile( ProfileLocator locator )
    {
        PsmlManager.removeDocument(locator);
    }

    /** Query for a collection of profiles given a profile locator criteria.
     *
     * @param locator The profile locator criteria.
     * @return The list of profiles matching the locator criteria.
     */
    public Iterator query( QueryLocator locator )
    {
        return PsmlManager.query( locator );
    }

    /**
     * @see org.apache.jetspeed.services.profiler.ProfilerService#useRoleProfileMerging
     */
    public boolean useRoleProfileMerging()
    {
        return this.useRoleFallback && this.useRoleMerge;
    }
}
