/*
 * Copyright 2000-2001,2004 The Apache Software Foundation.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.jetspeed.services.portaltoolkit;

//jetspeed stuff
import org.apache.jetspeed.portal.PortletControl;
import org.apache.jetspeed.portal.PortletController;
import org.apache.jetspeed.portal.PortletSkin;
import org.apache.jetspeed.portal.PortletSet;
import org.apache.jetspeed.portal.Portlet;
import org.apache.jetspeed.portal.PortletConfig;
import org.apache.jetspeed.portal.BasePortletConfig;
import org.apache.jetspeed.portal.PortletControlConfig;
import org.apache.jetspeed.portal.BasePortletControlConfig;
import org.apache.jetspeed.portal.PortletControllerConfig;
import org.apache.jetspeed.portal.BasePortletControllerConfig;
import org.apache.jetspeed.portal.BasePortletSkin;
import org.apache.jetspeed.portal.BasePortletSet;
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.Portlets;
import org.apache.jetspeed.om.profile.Layout;
import org.apache.jetspeed.om.profile.Profile;
import org.apache.jetspeed.om.profile.Parameter;
import org.apache.jetspeed.om.profile.MetaInfo;
import org.apache.jetspeed.om.profile.Entry;
import org.apache.jetspeed.om.profile.ProfileLocator;
import org.apache.jetspeed.om.profile.PSMLDocument;
import org.apache.jetspeed.services.Profiler;
import org.apache.jetspeed.services.rundata.JetspeedRunData;
import org.apache.jetspeed.services.rundata.JetspeedRunDataService;

import org.apache.jetspeed.services.Registry;
import org.apache.jetspeed.services.PortletFactory;
import org.apache.jetspeed.services.logging.JetspeedLogFactoryService;
import org.apache.jetspeed.services.logging.JetspeedLogger;
import org.apache.jetspeed.om.registry.PortletEntry;
import org.apache.jetspeed.om.registry.PortletControlEntry;
import org.apache.jetspeed.om.registry.PortletControllerEntry;
import org.apache.jetspeed.om.registry.SkinEntry;
import org.apache.jetspeed.util.MetaData;
import org.apache.jetspeed.util.JetspeedException;
import org.apache.jetspeed.util.ServiceUtil;
import org.apache.jetspeed.om.BaseSecurityReference;
import org.apache.jetspeed.om.SecurityReference;
import org.apache.jetspeed.om.registry.SecurityEntry;

import org.apache.turbine.services.TurbineServices;
import org.apache.turbine.services.TurbineBaseService;
import org.apache.turbine.services.InitializationException;
import org.apache.turbine.services.resources.ResourceService;
import org.apache.turbine.services.rundata.RunDataService;

import java.util.Iterator;

import java.util.Hashtable;
import java.util.Map;
import javax.servlet.ServletConfig;

/**
 * Simple implementation of the PortalFactoryService.
 *
 * @author <a href="mailto:raphael@apache.org">Raphaël Luta</a>
 * @author <a href="mailto:taylor@apache.org">David Sean Taylor</a>
 * @author <a href="mailto:morciuch@apache.org">Mark Orciuch</a> 
 *
 * @version $Id: JetspeedPortalToolkitService.java,v 1.33 2004/03/29 21:02:29 taylor Exp $
 */
public class JetspeedPortalToolkitService
    extends TurbineBaseService
    implements PortalToolkitService
{
    /**
     * Static initialization of the logger for this class
     */    
    private static final JetspeedLogger logger = JetspeedLogFactoryService.getLogger(JetspeedPortalToolkitService.class.getName());    

    /** The default control to use when none is specified */
    private String defaultControl = null;

    /** The default controller to use when none is specified */
    private String defaultController = null;

    /** The default skin to use when none is specified */
    private String defaultSkin = null;

    /** The default user security ref to use when none is specified */
    private String defaultUserSecurityRef = null;

    /** The default anonymous user security ref to use when none is specified */
    private String defaultAnonSecurityRef = null;

    /** The default role security ref to use when none is specified */
    private String defaultRoleSecurityRef = null;

    /** The default group security ref to use when none is specified */
    private String defaultGroupSecurityRef = null;

    /**
     * This is the early initialization method called by the
     * Turbine <code>Service</code> framework
     */
    public void init(ServletConfig conf) throws InitializationException
    {

        ResourceService serviceConf =
            ((TurbineServices) TurbineServices.getInstance()).getResources(
                PortalToolkitService.SERVICE_NAME);

        this.defaultControl = serviceConf.getString("default.control");
        this.defaultController = serviceConf.getString("default.controller");
        this.defaultSkin = serviceConf.getString("default.skin");
        this.defaultUserSecurityRef = serviceConf.getString("default.user.security.ref");
        this.defaultAnonSecurityRef = serviceConf.getString("default.anon.security.ref");
        this.defaultRoleSecurityRef = serviceConf.getString("default.role.security.ref");
        this.defaultGroupSecurityRef = serviceConf.getString("default.group.security.ref");
        setInit(true);

    }

    /**
     * Instanciates a PortletControl based on a Registry entry, if available
     * or directly from a classname.
     *
     * @param name a PortletControl name available in the registry or a classname
     * @return the created PortletControl
     */
    public PortletControl getControl(String name)
    {
        PortletControl pc = null;
        PortletControlEntry entry = null;

        if (name != null)
        {
            entry = (PortletControlEntry) Registry.getEntry(Registry.PORTLET_CONTROL, name);
        }

        Map params = null;

        try
        {
            if (entry == null)
            {
                if (name != null)
                {
                    pc = (PortletControl) Class.forName(name).newInstance();
                    params = new Hashtable();
                }
            }
            else
            {
                pc = (PortletControl) Class.forName(entry.getClassname()).newInstance();
                params = entry.getParameterMap();
            }
        }
        catch (Exception e)
        {
            logger.error("Unable to instanciate control " + name + ", using default", e);
        }

        if ((pc == null) && (defaultControl != null) && (!defaultControl.equals(name)))
        {
            return getControl(defaultControl);
        }

        PortletControlConfig pcConf = new BasePortletControlConfig();
        pcConf.setName(name);
        pcConf.setInitParameters(params);
        pc.setConfig(pcConf);

        return pc;
    }

    /**
     * Instanciates a PortletControl based on a PSML Control object
     *
     * @param control the PSML control object
     * @return the created PortletControl
     */
    public PortletControl getControl(Control control)
    {
        PortletControl pc = null;

        if (control != null)
        {
            pc = getControl(control.getName());
            pc.getConfig().getInitParameters().putAll(getParameters(control));
        }
        else
        {
            if (defaultControl != null)
            {
                pc = getControl(this.defaultControl);
            }
        }

        return pc;
    }

    protected PortletControl getControl(Control control, PortletEntry entry)
    {
        PortletControl pc = null;

        if (control != null)
        {
            pc = getControl(control.getName());
            pc.getConfig().getInitParameters().putAll(getParameters(control));
        }
        else
        {
            org.apache.jetspeed.om.registry.Parameter dftPortletCtrl =
                entry.getParameter("_control");

            if (dftPortletCtrl != null)
            {
                pc = getControl(dftPortletCtrl.getValue());
            }
            else if (defaultControl != null)
            {
                pc = getControl(this.defaultControl);
            }
        }

        return pc;
    }

    /**
     * Instanciates a PortletController based on a Registry entry, if available
     * or directly from a classname.
     *
     * @param name a PortletController name available in the registry or a classname
     * @return the created PortletController
     */
    public PortletController getController(String name)
    {
        PortletController pc = null;
        PortletControllerEntry entry = null;

        if (name != null)
        {
            entry = (PortletControllerEntry) Registry.getEntry(Registry.PORTLET_CONTROLLER, name);
        }

        Map params = null;

        try
        {
            if (entry == null)
            {
                if (name != null)
                {
                    pc = (PortletController) Class.forName(name).newInstance();
                    params = new Hashtable();
                }
            }
            else
            {
                pc = (PortletController) Class.forName(entry.getClassname()).newInstance();
                params = entry.getParameterMap();
            }
        }
        catch (Exception e)
        {
            logger.error("Unable to instanciate controller " + name + ", using default");
        }

        if ((pc == null) && (defaultController != null) && (!defaultController.equals(name)))
        {
            return getController(defaultController);
        }

        PortletControllerConfig pcConf = new BasePortletControllerConfig();
        pcConf.setName(name);
        pcConf.setInitParameters(params);
        pc.setConfig(pcConf);
        pc.init();

        return pc;
    }

    /**
     * Instantiates a PortletController based on a PSML Controller object
     *
     * @param controller the PSML controller object
     * @return the created PortletController
     */
    public PortletController getController(Controller controller)
    {

        PortletController pc = null;

        if (controller != null)
        {
            pc = getController(controller.getName());
            pc.getConfig().getInitParameters().putAll(getParameters(controller));
        }
        else
        {
            if (defaultController != null)
            {
                pc = getController(this.defaultController);
            }
        }

        pc.init();

        return pc;
    }

    /**
     * Create a PortletSkin object based on a Registry skin name
     *
     * @param name the registry SkinEntry name
     * @return the new PortletSkin object
     */
    public PortletSkin getSkin(String name)
    {
        BasePortletSkin result = new BasePortletSkin();

        SkinEntry entry = null;

        if (name != null)
        {
            entry = (SkinEntry) Registry.getEntry(Registry.SKIN, name);
        }

        // either we don't have any skin defined, the skin reference is null
        // or the skin reference is invalid, in all case, retrieve the default
        // skin entry
        if (entry == null)
        {
            entry = (SkinEntry) Registry.getEntry(Registry.SKIN, this.defaultSkin);
        }

        if (entry != null)
        {
            // build the PortletSkin object
            result.setName(entry.getName());
            result.putAll(entry.getParameterMap());
        }

        // Make the skin aware of what the user agent is capable of.
        JetspeedRunDataService jrds =
            (JetspeedRunDataService) ServiceUtil.getServiceByName(RunDataService.SERVICE_NAME);
        JetspeedRunData jData = jrds.getCurrentRunData();
        if(jData != null)
        {
        	result.setCapabilityMap(jData.getCapability()); 
        }
        return result;
    }

    /**
     * Create a PortletSkin object based on PSML skin description
     *
     * @param skin the PSML Skin object
     * @return the new PortletSkin object
     */
    public PortletSkin getSkin(Skin skin)
    {
        PortletSkin result = null;
        String name = null;

        if (skin != null)
        {
            name = skin.getName();

            // create the PortletSkin corresponding to this entry
            result = getSkin(name);

            // override the values with the locally defined properties
            result.putAll(getParameters(skin));

        }

        return result;
    }

    /**
     * Creates a PortletSet from a PSML portlets description
     *
     * @param portlets the PSML portlet set description
     * @return a new instance of PortletSet
     */
    public PortletSet getSet(Portlets portlets)
    {
        VariableInteger lastID = new VariableInteger(0);
        return getSet(portlets, new VariableInteger(0));
    }

    /**
     * Creates a PortletSet from a PSML portlets description, updating
     * the portletset name based on its position within the tree
     *
     * @param portlets the PSML portlet set description
     * @param count the portletset number within the complete tree
     * @return a new instance of PortletSet
     */
    protected PortletSet getSet(Portlets portlets, VariableInteger theCount)
    {
        // Create a new BasePortletSet to handle the portlets
        BasePortletSet set = new BasePortletSet();
        PortletController controller = getController(portlets.getController());
        set.setController(controller);
        String name = portlets.getName();
        if (name != null)
        {
            set.setName(name);
        }
        else
            set.setName(String.valueOf(theCount.getValue()));

        set.setID(portlets.getId());

        theCount.setValue(theCount.getValue() + 1);

        //FIXME: this sucks ! we should either associate the portlet set
        //with its portlets peer or set the porpoerties directly on the portlet
        //set object
        //Unfortunately, this would change the API too drastically for now...
        set.setPortletConfig(getPortletConfig(portlets));

        // Add all sub portlet sets in the main set
        //        Portlets[] subsets = portlets.getPortlets();
        //        for (int i=0; i < subsets.length; i++ )

        for (Iterator it = portlets.getPortletsIterator(); it.hasNext();)
        {
            Portlets subset = (Portlets) it.next();
            // Set this subset's parent Portlets collection.          
            subset.setParentPortlets(portlets);

            Map constraints = getParameters(subset.getLayout());
            int position = getPosition(subset.getLayout());
            set.addPortlet(
                getSet(subset, theCount),
                controller.getConstraints(constraints),
                position);
        }

        // Populate the PortletSet with Portlets
        //        Entry[] entries = portlets.getEntry();
        //        for( int i = 0; i < entries.length; ++i )

        for (Iterator eit = portlets.getEntriesIterator(); eit.hasNext();)
        {
            try
            {

                Entry psmlEntry = (Entry) eit.next();
                PortletEntry entry =
                    (PortletEntry) Registry.getEntry(Registry.PORTLET, psmlEntry.getParent());

                if (entry != null)
                {
                    Portlet p = PortletFactory.getPortlet(psmlEntry);

                    if (p != null)
                    {
                        Map constraints = getParameters(psmlEntry.getLayout());
                        int position = getPosition(psmlEntry.getLayout());

                        PortletControl control = getControl(psmlEntry.getControl(), entry);

                        set.addPortlet(
                            initControl(control, p),
                            controller.getConstraints(constraints),
                            position);
                    }
                }
                else
                {
                    logger.error(
                        " The portlet "
                            + psmlEntry.getParent()
                            + " does not exist in the Registry ");
                    continue;
                }
            }
            catch (JetspeedException e)
            {
                logger.error("Exception", e);
                continue;
            }

        }

        // Decorate with a control if required and return
        if (portlets.getControl() != null)
        {
            PortletControl control = getControl(portlets.getControl());
            return initControl(control, set);
        }

        set.sortPortletSet();
        // Or return the set
        return set;
    }

    /**
     * Associates a PortletControl with an existing Portlet and
     * returns the Control
     *
     * @param pc the existing PortletControl
     * @param portlet the existing Portlet to be associated with the control
     * @return first PortletControl associated with the portlet
     */
    protected PortletControl initControl(PortletControl pc, Portlet portlet)
    {

        if (portlet == null)
        {
            throw new IllegalArgumentException("Portlet not specified");
        }

        if (pc == null)
        {
            throw new IllegalArgumentException("PortletControl not specified");
        }

        pc.init(portlet);

        return pc;

    }

    /**
    Given a PSML Portlets, get the value of what its PortletConfig would be.
    
    @param entry the Portlets containing the config
    @return the newly created PortletConfig object
    */
    protected PortletConfig getPortletConfig(Portlets portlets)
    {

        PortletConfig pc = new BasePortletConfig();

        pc.setName(portlets.getName());
        pc.setInitParameters(getParameters(portlets));

        //Invocation of new skin-locating algorithim
        pc.setPortletSkin(getSkin(findSkin(portlets)));

        pc.setSecurityRef(portlets.getSecurityRef());
        pc.setMetainfo(getMetaData(portlets));

        return pc;
    }

    /**
     * Fetches the parameters out of a PSML Portlets entry
     *
     * @param portlets the Portlets entry to check for parameters
     * @return a Map containing the parameters names/values, an empty Dictionary
     *        is returned if there are no parameters
     */
    protected static Map getParameters(Portlets portlets)
    {
        Hashtable hash = new Hashtable();

        if (portlets != null)
        {
            Parameter[] props = portlets.getParameter();

            for (int i = 0; i < props.length; ++i)
            {
                hash.put(props[i].getName(), props[i].getValue());
            }
        }

        return hash;
    }

    /**
     * Retrieves the parameters from a PSML Control object
     *
     * @param control the PSML object to explore
     * @return a Map of the existing control parameters or an empty map
     */
    protected static Map getParameters(Control control)
    {
        Hashtable hash = new Hashtable();

        if (control != null)
        {
            Parameter[] params = control.getParameter();

            for (int i = 0; i < params.length; i++)
            {
                hash.put(params[i].getName(), params[i].getValue());
            }
        }
        return hash;
    }

    /**
     * Retrieves the parameters from a PSML Controller object
     *
     * @param controller the PSML object to explore
     * @return a Map of the existing controller parameters or an empty map
     */
    protected static Map getParameters(Controller controller)
    {
        Hashtable hash = new Hashtable();

        if (controller != null)
        {
            Parameter[] params = controller.getParameter();

            for (int i = 0; i < params.length; i++)
            {
                hash.put(params[i].getName(), params[i].getValue());
            }
        }
        return hash;
    }

    /**
     * Retrieves a parameter Map from an array of PSML Layout object
     *
     * @param layout the Layout object to use
     * @return a Map containing the names/values, an empty map
     *  is returned if there are no properties
     */
    protected static Map getParameters(Layout layout)
    {
        Hashtable hash = new Hashtable();

        if (layout != null)
        {
            Parameter[] props = layout.getParameter();

            for (int i = 0; i < props.length; ++i)
            {
                hash.put(props[i].getName(), props[i].getValue());
            }
        }

        return hash;
    }

    /**
     * Retrieves a parameter Map from a PSML skin object
     *
     * @param skin the Skin object to use
     * @return a Map containing the names/values, an empty map
     *  is returned if there are no properties
     */
    protected static Map getParameters(Skin skin)
    {
        Hashtable hash = new Hashtable();

        if (skin != null)
        {
            Parameter[] props = skin.getParameter();

            for (int i = 0; i < props.length; ++i)
            {
                hash.put(props[i].getName(), props[i].getValue());
            }
        }

        return hash;
    }

    /**
    Create a MetaData object from a PSML Metainfo object
    
    @param meta the Metainfo to copy
    
    @return the new MetaData object, empty if meta is null
    */
    protected static MetaData getMetaData(Portlets portlets)
    {
        MetaData data = new MetaData();
        MetaInfo meta = portlets.getMetaInfo();

        if (meta != null)
        {
            if (meta.getTitle() != null)
            {
                data.setTitle(meta.getTitle());
            }

            if (meta.getDescription() != null)
            {
                data.setDescription(meta.getDescription());
            }

            if (meta.getImage() != null)
            {
                data.setImage(meta.getImage());
            }
        }

        return data;

    }

    /**
     * Get the position value in a Layout object
     *
     * @param layout the Layout object to use
     *
     * @return the defined position or -1 if undefined
     */
    protected static int getPosition(Layout layout)
    {
        int pos = -1;

        try
        {
            pos = (int) layout.getPosition();
        }
        catch (RuntimeException e)
        {
            // either layout is null or the position isn't an integer
            // keep the default value
        }

        return pos;
    }

    protected static class VariableInteger
    {
        int value;

        public VariableInteger(int value)
        {
            this.value = value;
        }

        public int getValue()
        {
            return this.value;
        }

        public void setValue(int value)
        {
            this.value = value;
        }
    }

    /**
     * Given a locator String path, returns a Portlets collecton
     *
     * @param locatorPath ProfileLocator resource path identifier
     * @return a portlets collection from the PSML resource
     */
    public Portlets getReference(String locatorPath)
    {
        ProfileLocator locator = Profiler.createLocator();
        locator.createFromPath(locatorPath);
        String id = locator.getId();

        try
        {
            Profile profile = Profiler.getProfile(locator);
            PSMLDocument doc = profile.getDocument();
            if (doc == null)
            {
                return null;
            }
            Portlets portlets = doc.getPortlets();
            return portlets;
        }
        catch (Exception e)
        {
            logger.error("Exception", e);
            return null;
        }
    }

    /**
     * Helps locate a skin, recursively if neccesary.
     * <ul>
     *  <li>First: return the name of the skin defined for this <code>Portlets</code>
     * collection.</li>
     * <li>  If the this <code>Portlets</code> collection has no skin defined, it's
     *  parent is checked, then it's parent's parent and so on until either a skin
     *  is found.</li>
     * <li> If the previous two attempts fail the, the system default skin is used</li>
     * @param Portlets portlets Portlets collection whose skin needs to be located.
     */
    protected String findSkin(Portlets portlets)
    {
        if (portlets.getSkin() != null)
        {
            return portlets.getSkin().getName();
        }
        else if (portlets.getParentPortlets() != null)
        {
            return findSkin(portlets.getParentPortlets());
        }
        else
        {
            return this.defaultSkin;
        }
    }

    /**
     * Gets default security ref based on the profile type (user|role|group). Returns
     * null if no default is defined.
     * 
     * @param profile
     * @return default security reference
     */
    public SecurityReference getDefaultSecurityRef(Profile profile)
    {
        String type = null;
        if (profile.getUserName() != null)
        {
            if (profile.getAnonymous())
            {
                type = Profiler.PARAM_ANON;
            }
            else
            {
                type = Profiler.PARAM_USER;
            }
        }
        else if (profile.getRoleName() != null)
        {
            type = Profiler.PARAM_ROLE;
        }
        else if (profile.getGroupName() != null)
        {
            type = Profiler.PARAM_GROUP;
        }

        return getDefaultSecurityRef(type);

    }

    /**
     * Gets default security ref based on the profile type (user|role|group). Returns
     * null if no default is defined.
     *
     * @param type of entity to return default security ref for
     * @return default security reference
     */
    public SecurityReference getDefaultSecurityRef(String type)
    {
        BaseSecurityReference result = null;

        SecurityEntry entry = null;

        String defaultRef = null;
        if (type.equals(Profiler.PARAM_USER))
        {
            defaultRef = this.defaultUserSecurityRef;
        }
        else if (type.equals(Profiler.PARAM_ANON))
        {
            defaultRef = this.defaultAnonSecurityRef;
        }
        else if (type.equals(Profiler.PARAM_ROLE))
        {
            defaultRef = this.defaultRoleSecurityRef;
        }
        else if (type.equals(Profiler.PARAM_GROUP))
        {
            defaultRef = this.defaultGroupSecurityRef;
        }

        if (defaultRef != null)
        {
            entry = (SecurityEntry) Registry.getEntry(Registry.SECURITY, defaultRef);
            if (logger.isDebugEnabled())
            {
                logger.debug(
                    "JetspeedPortalToolkit: default security for type: " + type + " is " + defaultRef);
            }
            if (entry != null)
            {
                result = new BaseSecurityReference();
                result.setParent(entry.getName());
                if (logger.isDebugEnabled())
                {
                    logger.debug(
                        "JetspeedPortalToolkit: default security for type: "
                            + type
                            + " was set to "
                            + entry.getName());
                }
            }
        }

        return result;

    }
}
