/*
 * 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.util.template;

import org.apache.ecs.ConcreteElement;
import org.apache.ecs.StringElement;

import org.apache.turbine.util.RunData;
import org.apache.turbine.services.pull.ApplicationTool;
import org.apache.turbine.services.localization.Localization;

import org.apache.jetspeed.portal.Portlet;
import org.apache.jetspeed.portal.PortletSet;
import org.apache.jetspeed.portal.PortletControl;
import org.apache.jetspeed.services.PortalToolkit;
import org.apache.jetspeed.services.PortletFactory;
import org.apache.jetspeed.services.logging.JetspeedLogFactoryService;
import org.apache.jetspeed.services.logging.JetspeedLogger;
import org.apache.jetspeed.services.resources.JetspeedResources;
import org.apache.jetspeed.services.Profiler;
import org.apache.jetspeed.services.rundata.JetspeedRunData;
import org.apache.jetspeed.om.SecurityReference;
import org.apache.jetspeed.om.profile.PSMLDocument;
import org.apache.jetspeed.om.profile.Profile;
import org.apache.jetspeed.om.profile.ProfileException;
import org.apache.jetspeed.om.profile.Entry;
import org.apache.jetspeed.modules.ParameterLoader;
import org.apache.jetspeed.services.security.PortalResource;
import org.apache.jetspeed.om.registry.Parameter;
import org.apache.jetspeed.om.registry.PortletEntry;
import org.apache.jetspeed.om.security.JetspeedUser;
import org.apache.jetspeed.services.Registry;
import org.apache.jetspeed.services.JetspeedSecurity;
import org.apache.jetspeed.portal.security.portlets.PortletWrapper;
import org.apache.jetspeed.util.JetspeedClearElement;

import java.util.Enumeration;
import java.util.Stack;
import java.util.Map;
import java.util.Hashtable;
import java.util.StringTokenizer;

/**
 * Utility class for accessing Jetspeed in a "pull" mode
 *
 * <strong>Since the tool stores a RunData object, it may not be
 * shared between threads and/or requests</strong>
 *
 * @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="mark_orciuch@ngsltd.com">Mark Orciuch</a>
 * @author <a href="mailto:weaver@apache.org">Scott T. Weaver</a>
 *
 * @version $Id: JetspeedTool.java,v 1.38 2004/03/29 21:38:43 taylor Exp $
 */
public class JetspeedTool implements ApplicationTool
{
    /**
     * Static initialization of the logger for this class
     */    
    private static final JetspeedLogger logger = JetspeedLogFactoryService.getLogger(JetspeedTool.class.getName());
    
    /** RunData object for this request */
    protected JetspeedRunData rundata = null;
    
    /** Empty constructor used by introspection
     */
    public JetspeedTool()
    {
    }

    /** The Tool constructor
     *
     * @param data the RunData object for the current request
     */
    public JetspeedTool(RunData data)
    {
        this.rundata = (JetspeedRunData)data;
    }
    
    /**
     * This will initialise a JetspeedTool object that was
     * constructed with the default constructor (ApplicationTool
     * method).
     *
     * @param data assumed to be a RunData object
     */
    public void init(Object data)
    {
        this.rundata = (JetspeedRunData)data;
    }

    /**
     * Refresh method - does nothing
     */
    public void refresh()
    {
        // empty
    }

    /** 
     * Returns the portlet content customized for the current user.
     * Currently, the layout engine does not handle panes, so the 
     * panes are mapped to real PSML files.
     * If the pane name is null or "default", the profiler will automatically
     * chose the PSML content, else the tool will try to load the PSML
     * file with the specified name
     *
     * @param name the name of the pane to render
     * @return the rendered content of the pane
     */
    public ConcreteElement getPane(String name)
    {
        ConcreteElement result = null;
        String msg = "";
        
        if (null != rundata)
        {
            Profile profile = rundata.getProfile();
            try
            {
                if (null == profile)
                {
                    profile = Profiler.getProfile(rundata);
                    if (profile == null)
                    {
                        throw new ProfileException("Profile not found.");
                    }
                    rundata.setProfile(profile);
                }
            
                if ((name == null) ||  Profiler.DEFAULT_PROFILE.equals(name) || "".equals(name))
                    ;
                else
                    profile.setName(name);
     
                PSMLDocument doc = profile.getDocument();
                if (null != doc)
                {
                    result = PortalToolkit.getSet(doc.getPortlets()).getContent(rundata);
                }
            }
            catch (Exception e)
            {
                logger.warn("JetspeedTool.getPane: problem getting: "
                            + name + " from current request's profile: " + e.toString());
                msg = e.getMessage();
            }

        }
        
        if (result == null)
        {
            result = new StringElement("Error retrieving Portal Page: " + msg);
        }

        return result;
    }

    /**
     * Return the content of a named portlet. This portlet is sought in
     * the current PSML resource.
     *
     * If a control is attached to the portlet description, returns the defined
     * portlet and control, otherwise use the default control.
     *
     * Note:  This will return the FIRST portlet with a name = name.  Use getPortletById().
     *
     * @param name the name of the portlet to render
     * @return the rendered content of the portlet
     *
     * @deprecated Use getPortletById()
     */
    public ConcreteElement getPortlet(String name)
    {
        ConcreteElement result = null;
        Portlet found = null;
        Stack sets = new Stack();
        sets.push(rundata.getProfile().getRootSet());
        
        while ((sets.size() > 0) && (found==null))
        {
            PortletSet set = (PortletSet)sets.pop();
            
            if (set.getName().equals(name))
            {
                found = set;
            }
            else
            {
                Enumeration en = set.getPortlets();
                while((found==null) && en.hasMoreElements())
                {
                    Portlet p = (Portlet)en.nextElement();
                        
                    // unstack the controls to find the real PortletSets
                    Portlet real = p;
                    while (real instanceof PortletControl)
                    {
                        real = ((PortletControl)p).getPortlet();
                    }
                        
                    if (real instanceof PortletSet)
                    {
                        // we'll explore this set afterwards
                        sets.push(real);
                    }
                    else if (p.getName().equals(name))
                    {                        
                        found = p;
                    }
                }
            }
        }
        
        if (found!=null)
        {
            result = found.getContent(rundata);
        }
        
        if (result==null)
        {
            //the customizer already streamed its content, return a stub
            result = new ConcreteElement();
        }

        return result;
    }

    /** This method retrieves the appropriate customizer portlet for the 
     *  current portlet
     *
     *  @param p the portlet to customize
     *  @param data the RunData for this request
     *  @return the portlet object of the appropriate customizer
     */
    public static Portlet getCustomizer(Portlet p)
    {
        Portlet customizer = p;

        while (p instanceof PortletControl)
        {
            p = ((PortletControl)p).getPortlet();
        }
        
        // if the portlet cannot customize itself...
        if ( !p.providesCustomization() )
        {

            //look for the customizer name in the portlet
            //config (from Registry definition)
        
            String name = p.getPortletConfig().getInitParameter("_customizer");
            
            if (name == null)
            {
                String key = (p instanceof PortletSet)?"PortletSet":"Portlet";            

                name = JetspeedResources.getString("customizer."+key,key+"Customizer");
            }
        
            try
            {
                customizer = PortletFactory.getPortlet(name, p.getID()+"customize");
                customizer.getPortletConfig()
                          .setPortletSkin(p.getPortletConfig().getPortletSkin());
                PortletControl control = PortalToolkit.getControl((String)null);
                if (control!=null)
                {
                    control.setPortlet(customizer);
                    control.init();
                    return control;
                }
            }
            catch (Exception e)
            {
                logger.error("Exception",  e);
            }
        }
            
        return customizer;
    }

    /** This method retrieves the appropriate information portlet for the 
      *  current portlet
      *
      *  @param p the portlet to display information about
      *  @param data the RunData for this request
      *  @return the portlet object of the appropriate customizer
      */
     public static Portlet getPortletInfoPortlet(RunData data)
     {
         Portlet info = null;
         
         String name = JetspeedResources.getString("PortletInfoPortlet.name", "PortletInfoPortlet");
 
         try {
 
             if (null != data)
             {
                 JetspeedRunData jdata = (JetspeedRunData)data;
                 Profile profile = jdata.getProfile();
 
                 if (null == profile)
                 {
                     logger.warn("JetspeedTool: profile is null");
                     profile = Profiler.getProfile(jdata);
                     jdata.setProfile(profile);
                 }
 
                 Portlet source = findPortlet(data);
                 if (source != null) {
                     jdata.setPortlet(source.getName());
                     info = PortletFactory.getPortlet(name, "PortletInfoPortlet");
                     info.getPortletConfig().setPortletSkin(source.getPortletConfig().getPortletSkin());
                     PortletControl control = PortalToolkit.getControl((String)null);
                     if (control != null) 
                     {
                         control.setPortlet(info);
                         control.init();
                         return control;
                     }
                 }
             }
         } catch (Exception e) {
             logger.error("Exception",  e);
         }
             
         return info;
     }
 
     /**
      * Finds portlet identified by js_peid in the current user's profile
      * 
      * @param rundata for this request
      * @return portlet identified by js_peid
      */
     private static Portlet findPortlet(RunData rundata) {
 
         Portlet found = null;
         JetspeedRunData jdata = (JetspeedRunData)rundata;
         String peid = jdata.getJs_peid();
         if (peid != null) 
         {
             Stack sets = new Stack();
             sets.push(jdata.getProfile().getRootSet());
 
             while ((found==null) && (sets.size() > 0))
             {
                 PortletSet set = (PortletSet)sets.pop();
 
                 if (set.getID().equals(peid))
                 {
                     found = set;
                 }
                 else
                 {
                     Enumeration en = set.getPortlets();
                     while((found==null) && en.hasMoreElements())
                     {
                         Portlet p = (Portlet)en.nextElement();
 
                         // unstack the controls to find the real PortletSets
                         Portlet real = p;
                         while (real instanceof PortletControl)
                         {
                             real = ((PortletControl)p).getPortlet();
                         }
 
                         if (real instanceof PortletSet)
                         {
                             if (real.getID().equals(peid))
                             {
                                 found=real;
                             }
                             else
                             {
                                 // we'll explore this set afterwards
                                 sets.push(real);
                             }
                         }
                         else if (p.getID().equals(peid))
                         {                        
                             found = p;
                         }
                     }
                 }
             }
         }
 
         return found;
     }


    /** 
     * Return the content of a portal element given the id of the element. 
     *
     * @param id The portlet id
     * @return the rendered content of the portlet
     */
    public ConcreteElement getPortalElement(String id)
    {
        ConcreteElement result = null;
        
        if (null != rundata)
        {
            Profile profile = rundata.getProfile();
            try
            {
                if (null == profile)
                {
                    System.out.println("profile is null");
                    profile = Profiler.getProfile(rundata);
                    rundata.setProfile(profile);
                }
 
                PSMLDocument doc = profile.getDocument();
                if (null != doc)
                {                
                    Entry entry = doc.getEntryById(id);
                    if (null == entry)
                    {
                        // FIXME: need to write this function
                        // Portlets ps = doc.getPortletsById(id);
                        result = new  StringElement("not implemented - PortletElement");
                    }
                    else
                    {
                        Portlet p = PortletFactory.getPortlet( entry );
                        if (p != null)
                        {
                            result = p.getContent(rundata);
                        }
                        else
                           result = new  StringElement("Error retrieving PortletElement");

                    }
                }                
            }
            catch (Exception e)
            {
                logger.error("Exception",  e);
            }
        }
        
        if (result == null)
        {
            result = new StringElement("Error fetching pane");
        }

        return result;

    }

    /**
     * Return the content of a portlet using the portlet's id (PEID). This portlet is sought in
     * the current PSML resource.
     *
     * If a control is attached to the portlet description, returns the defined
     * portlet and control, otherwise use the default control.
     *
     * @param peid the peid of the portlet to render
     * @return the rendered content of the portlet
     */
    public ConcreteElement getPortletById(String peid)
    {
        ConcreteElement result = null;
        Portlet found = null;
        Stack sets = new Stack();
        sets.push(rundata.getProfile().getRootSet());
        
        while ((sets.size() > 0) && (found==null))
        {
            PortletSet set = (PortletSet)sets.pop();
            
            if (set.getID().equals(peid))
            {
                found = set;
            }
            else
            {
                Enumeration en = set.getPortlets();
                while((found==null) && en.hasMoreElements())
                {
                    Portlet p = (Portlet)en.nextElement();
                        
                    // unstack the controls to find the real PortletSets
                    Portlet real = p;
                    while (real instanceof PortletControl)
                    {
                        real = ((PortletControl)p).getPortlet();
                    }
                        
                    if (real instanceof PortletSet)
                    {
                        // we'll explore this set afterwards
                        sets.push(real);
                    }
                    else if (p.getID().equals(peid))
                    {                        
                        found = p;
                    }
                }
            }
        }
        
        if (found!=null)
        {
            // Return portlet's content checking the security first
            result = PortletWrapper.wrap(found).getContent(rundata);
        }
        
        if (result==null)
        {
            //the customizer already streamed its content, return a stub
            result = new ConcreteElement();
        }

        return result;
    }

    /**
     * Return the content of a portlet using the portlet's name. This portlet is sought in
     * the registry. This is useful when you want to get portlet's content without
     * actually having the portlet in user's profile (for example, to preview a portlet
     * before adding it to the profile).
     * <P>
     * If a control name is specified to the portlet description, returns the defined
     * portlet and control, otherwise use the default control.
     * <P>
     * Issues to resolve:
     * <UL>
     * <LI>is new portlet instance created everytime someone previews the same portlet?</LI>
     * <LI>should use the same skin as the current pane</LI>
     * <LI>if TitlePortletControl is used, the action icons (max, min, etc) are not functional.
     * Also, customize icon should not be present.</LI>
     * <LI> interactive portlets (such as DatabaseBrowser) lose functionality (such as sorting
     * in DatabaseBrowser).</LI>
     * </UL>
     * 
     * @param portletName
     *               Name of the portlet as defined in registry
     * @param controlName
     *               Optional control name to use in displaying the portlet
     * @return the rendered content of the portlet
     */
    public ConcreteElement getPortletFromRegistry(RunData data)
    {

        ConcreteElement result = null;
        Portlet p = null;
        String portletName = data.getParameters().getString("p"); 
        String controlName = data.getParameters().getString("c");
        
        try 
        {

            // Retrieve registry entry
            PortletEntry entry = (PortletEntry) Registry.getEntry(Registry.PORTLET, portletName);

            // Verify security for the parameter
            boolean canAccess = JetspeedSecurity.checkPermission((JetspeedUser) data.getUser(), 
                                                                 new PortalResource(entry), 
                                                                 JetspeedSecurity.PERMISSION_CUSTOMIZE);

            if (canAccess)
            {
                // Always set portlet id to "preview" so each preview request gets it from the cache.
                // At least, I think that's how it works.
                p = PortletFactory.getPortlet(portletName, "preview");
                PortletControl control = controlName == null ? PortalToolkit.getControl((String) null) 
                                                             : PortalToolkit.getControl(controlName);
                if (control != null)
                {
                    JetspeedRunData jdata = (JetspeedRunData) rundata;
                    // Use the profile's skin
                    p.getPortletConfig().setPortletSkin(PortalToolkit.getSkin(jdata.getProfile().getDocument().getPortlets().getSkin()));
                    control.setPortlet(p);
                    control.init();
                    result = control.getContent(rundata);
                } 
                else if (p != null)
                {
                    result = p.getContent(rundata);
                }
            }
            else
            {
                result = new JetspeedClearElement(Localization.getString(data, "SECURITY_NO_ACCESS_TO_PORTLET"));
            }
        } 
        catch (Exception e) 
        {
            logger.error("Exception",  e);
            result = new ConcreteElement();
        }
                
        if (result == null)
        {
            //the customizer already streamed its content, return a stub
            result = new ConcreteElement();
        }

        return result;
    }

    /**
     * Return the content of a portlet using the portlet's name. This portlet is sought in
     * the registry. This is useful when you want to get portlet's content without
     * actually having the portlet in user's profile (for example, to preview a portlet
     * before adding it to the profile).
     * <P>
     * If a control name is specified to the portlet description, returns the defined
     * portlet and control, otherwise use the default control.
     * <P>
     * Issues to resolve:
     * <UL>
     * <LI>is new portlet instance created everytime someone previews the same portlet?</LI>
     * <LI>should use the same skin as the current pane</LI>
     * <LI>if TitlePortletControl is used, the action icons (max, min, etc) are not functional.
     * Also, customize icon should not be present.</LI>
     * <LI> interactive portlets (such as DatabaseBrowser) lose functionality (such as sorting
     * in DatabaseBrowser).</LI>
     * </UL>
     * 
     * @param portletName
     *               Name of the portlet as defined in registry
     * @param controlName
     *               Optional control name to use in displaying the portlet
     * @return the rendered content of the portlet
     * @deprecated Do not use this method because it's not secure. It will be removed after Beta 5.
     */
    public ConcreteElement getPortletFromRegistry(String portletName, String controlName)
    {

        ConcreteElement result = null;
        Portlet p = null;
        
        try 
        {
            // Always set portlet id to "preview" so each preview request gets it from the cache.
            // At least, I think that's how it works.
            p = PortletFactory.getPortlet(portletName, "preview");
            PortletControl control = controlName == null ? PortalToolkit.getControl((String) null) 
                                                         : PortalToolkit.getControl(controlName);
            if (control != null)
            {
                JetspeedRunData jdata = (JetspeedRunData) rundata;
                // Use the profile's skin
                p.getPortletConfig().setPortletSkin(PortalToolkit.getSkin(jdata.getProfile().getDocument().getPortlets().getSkin()));
                control.setPortlet(p);
                control.init();
                result = control.getContent(rundata);
            } 
            else if (p != null)
            {
                result = p.getContent(rundata);
            }
        } 
        catch (Exception e) 
        {
            logger.error("Exception",  e);
            result = new ConcreteElement();
        }
                
        if (result == null)
        {
            //the customizer already streamed its content, return a stub
            result = new ConcreteElement();
        }

        return result;
    }

    /**
     * Returns a parameter in its defined parameter style
     * 
     * @param data     for this request
     * @param portlet  portlet instance
     * @param parmName parameter name
     * @return current parameter value using specified presentation style
     */
    public static String getPortletParameter(RunData data, Portlet portlet, String parmName)
    {

        if (portlet != null && parmName != null) 
        {
            String parmValue = portlet.getPortletConfig().getInitParameter(parmName, "");
            return getPortletParameter(data, portlet, parmName, parmValue);
        }

        return "";
    }

    /**
     * Returns a parameter in its defined parameter style
     * 
     * @param data      for this request
     * @param portlet   portlet instance
     * @param parmName  parameter name
     * @param parmValue current parameter value
     * @return current parameter value using specified presentation style
     */
    public static String getPortletParameter(RunData data, Portlet portlet, String parmName, String parmValue)
    {
        String result = null;
        try 
        {
            if (portlet != null && parmName != null) 
            {
                // Retrieve registry entry and its parameter
                PortletEntry entry = (PortletEntry) Registry.getEntry(Registry.PORTLET, portlet.getName());
                Parameter param = entry.getParameter(parmName);

                // Verify security for the parameter
                boolean canAccess = JetspeedSecurity.checkPermission((JetspeedUser) data.getUser(), 
                                                                     new PortalResource(entry, param), 
                                                                     JetspeedSecurity.PERMISSION_CUSTOMIZE);
                Map portletParms = portlet.getPortletConfig().getInitParameters();
                String parmStyle = portlet.getPortletConfig().getInitParameter(parmName + ".style");

                // Add portlet reference
                portletParms.put(parmName.concat(".style.portlet"), portlet);

                if (canAccess)
                {
                    if (parmStyle != null) 
                    {
                        result = ParameterLoader.getInstance().eval(data, 
                                                                    parmStyle, 
                                                                    parmName, 
                                                                    parmValue, 
                                                                    portletParms);                
                    }
                    else
                    {
                        result = "<input type=\"text\" name=\"" + parmName + "\" value=\"" + parmValue + "\"";
                    }
                }
                else
                {
                    // If security does not allow access to specific parameter, allow to provide a fallback parameter
                    String parmNameNoAccess = portlet.getPortletConfig().getInitParameter(parmName + ".style.no-access");
                    if (parmNameNoAccess != null) 
                    {
                        if (logger.isDebugEnabled())
                        {
                            logger.debug("JetspeedTool: access to parm [" + parmName + "] disallowed, redirecting to parm [" + 
                                      parmNameNoAccess + "]");
                        }
                        String parmStyleNoAccess = portlet.getPortletConfig().getInitParameter(parmNameNoAccess + ".style");
                        result = ParameterLoader.getInstance().eval(data, 
                                                                    parmStyleNoAccess, 
                                                                    parmNameNoAccess, 
                                                                    parmValue, 
                                                                    portletParms);                
                    }
                }
            }
        } 
        catch (Exception e)
        {
            logger.error("Exception",  e);
        }

        return result;
    }
    
    /**
     * Returns a parameter rendered in specific parameter style
     * 
     * @param data      for this request
     * @param parmStyle parameter style
     * @param parmName  parameter name
     * @param parmValue current parameter value
     * @param options   optional style parameters in delimited format (option1=value1;option2=value2)
     * @return current parameter value using specified presentation style
     */
    public static String getParameter(RunData data, String parmStyle, String parmName, String parmValue, String parmOptions)
    {
        String result = null;
        try 
        {
            if (parmName != null) 
            {
                if (parmStyle != null) 
                {
                    Map options = null;
                    if (parmOptions != null && parmOptions.length() > 0)
                    {
                        options = new Hashtable();

                        StringTokenizer st = new StringTokenizer(parmOptions, ";");
                        String prefix = parmName + ".style.";
                        while (st.hasMoreTokens())
                        {
                            StringTokenizer pair = new StringTokenizer(st.nextToken(), "=");
                            if (pair.countTokens() == 2)
                            {
                                options.put(prefix + pair.nextToken().trim(), pair.nextToken().trim());
                            }
                        }

                    }
                    result = ParameterLoader.getInstance().eval(data, 
                                                                parmStyle, 
                                                                parmName, 
                                                                parmValue, 
                                                                options);                
                }
                else
                {
                    result = "<input type=\"text\" name=\"" + parmName + "\" value=\"" + parmValue + "\"";
                }                
            }
        } 
        catch (Exception e)
        {
            logger.error("Exception",  e);
            result = "<input type=\"text\" name=\"" + parmName + "\" value=\"" + parmValue + "\"";
        }

        return result;
    }

    /**
     * Retreives the correct SecurityReference for the portlet based on the current
     * profile and the request.
     */
    public SecurityReference getSecurityReference(Entry entry)
    {
        return JetspeedSecurity.getSecurityReference(entry, rundata);
    }
    
    public int getSecuritySource(Entry entry)
    {
        return JetspeedSecurity.getSecuritySource(entry, rundata);
    }
    
    /**
     * Retreives the Entry object for current portlet based on the 
     * "js_peid" parameter
     */
    public Entry getEntryFromRequest() throws Exception
    {
        String jsPeid = rundata.getParameters().getString("js_peid");
        Profile profile = Profiler.getProfile(rundata);
        PSMLDocument doc = profile.getDocument();
        return doc.getEntryById(jsPeid);
    }

}
