/*
 * Copyright 2000-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.portletfactory;

//jetspeed stuff
import org.apache.jetspeed.portal.Portlet;
import org.apache.jetspeed.portal.PortletConfig;
import org.apache.jetspeed.portal.PortletException;
import org.apache.jetspeed.portal.BasePortletConfig;

import org.apache.jetspeed.portal.security.portlets.PortletWrapper;

import org.apache.jetspeed.om.SecurityReference;
import org.apache.jetspeed.om.profile.Entry;
import org.apache.jetspeed.om.profile.Parameter;
import org.apache.jetspeed.om.profile.MetaInfo;
import org.apache.jetspeed.services.JetspeedSecurity;
import org.apache.jetspeed.services.Registry;
import org.apache.jetspeed.services.PortalToolkit;
import org.apache.jetspeed.services.logging.JetspeedLogFactoryService;
import org.apache.jetspeed.services.logging.JetspeedLogger;
import org.apache.jetspeed.services.portletcache.PortletCache;
import org.apache.jetspeed.services.portletcache.Cacheable;
import org.apache.jetspeed.om.registry.PortletEntry;
import org.apache.jetspeed.om.profile.Profile;
import org.apache.jetspeed.util.MetaData;
import org.apache.jetspeed.services.rundata.JetspeedRunDataService;
import org.apache.jetspeed.services.rundata.JetspeedRunData;

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.Hashtable;
import java.util.Iterator;
import java.util.Map;
import java.util.HashMap;
import javax.servlet.ServletConfig;

/**
 * Simple implementation of the PortalFactoryService.
 * 
 * @author <a href="mailto:raphael@apache.org">Raphaël Luta</a>
 * @author <a href="mailto:weaver@apache.org">Scott T. Weaver</a>
 * @version $Id: JetspeedPortletFactoryService.java,v 1.23 2004/02/23 03:36:42 jford Exp $
 */
public class JetspeedPortletFactoryService extends TurbineBaseService
    implements PortletFactoryService
{
    /**
     * Static initialization of the logger for this class
     */    
    private static final JetspeedLogger logger = JetspeedLogFactoryService.getLogger(JetspeedPortletFactoryService.class.getName());
    
    /** The default control to use when none is specified */
    private boolean enableCache = false;

    /** The JetspeedRunData Service. */
    private JetspeedRunDataService runDataService = 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(PortletFactoryService.SERVICE_NAME);

        this.enableCache = serviceConf.getBoolean("enable.cache",true);
        
        // get the runData service
        this.runDataService =
            (JetspeedRunDataService)TurbineServices.getInstance()
                .getService(RunDataService.SERVICE_NAME);

        setInit(true);
    }
            
    /**
     * Given a PSML Entry return an instanciated Portlet.
     *
     * @param entry a PSML Entry describing a portlet
     * @param id the PSML entry's portlet id     
     * @return an instanciated portlet corresponding to this entry
     */
    public Portlet getPortlet( Entry entry ) throws PortletException
    {
        PortletEntry regEntry = (PortletEntry)Registry.getEntry(Registry.PORTLET, 
                                                                entry.getParent() );
        if (regEntry == null)
        {
            throw new PortletException("PortletFactory: unknown portlet entry in Registry: "+entry.getParent());
        }
        
        if (PortletEntry.TYPE_ABSTRACT.equals(regEntry.getType()))
        {
            throw new PortletException("PortletFactory: can't instanciate abstract registry entry: "+regEntry.getName());
        }
            
        PortletConfig pc = getPortletConfig(regEntry, entry.getId());

        // Set portlet config with values from PSML Entry
        pc.getInitParameters().putAll(getParameters(entry));
        pc.setPortletSkin( PortalToolkit.getSkin( entry.getSkin() ) );
        pc.setSecurityRef( getSecurityReference(entry, regEntry));

        return getPortlet( getClassname(regEntry), pc, entry.getId() );
    }

    /**
     * Given a Portlet registry entry name, instanciate it
     *
     * @param name the name of a portlet in the registry
     * @return an instanciated portlet corresponding to this entry
     */
    public Portlet getPortlet( String name, String id ) throws PortletException
    {
        PortletEntry regEntry = (PortletEntry)Registry.getEntry(Registry.PORTLET, name );

        if (regEntry == null)
        {
            throw new PortletException("PortletFactory: unknown portlet entry in Registry: "+name);
        }
        
        if (PortletEntry.TYPE_ABSTRACT.equals(regEntry.getType()))
        {
            throw new PortletException("PortletFactory: can't instanciate abstract registry entry: "+name);
        }
            
        PortletConfig pc = getPortletConfig(regEntry, id);
        
        return getPortlet( getClassname(regEntry), pc, null );
    }
    
    /** 
     * Instanciates or retrieve from memory cache a portlet corresponding to the 
     * passed parameters
     *
     * @param classname the classname of the portlet to instanciate
     * @param pc the PortletConfig object to be associated with this object
     * @param id the PSML entry's portlet id
     * @return the Portlet created or retrieve from cache
     */
    protected Portlet getPortlet( String classname, PortletConfig pc, String id )
        throws PortletException
    {

        //record the begining of the portlet creation
        long begin = System.currentTimeMillis();

        Portlet portlet = null;
        Class portletClass = null;
        String handle = null;
        
        try
        {
            portletClass = Class.forName(classname);
        }
        catch (Exception e)
        {
            throw new PortletException( "PortletFactory: Unable to load class " + classname );
        }
        
        if (enableCache)
        {
            try
            {
                // try to invoke a static getHandle() for this class
                Class[] signatureParams = { Object.class };
                Object[] methodParams = { pc };
                handle = (String)portletClass.getMethod("getHandle",signatureParams)
                                             .invoke(null,methodParams);
                // make sure the handle is differenciated by class
                handle = String.valueOf(classname.hashCode())+handle;
            }
            catch (NoSuchMethodException e)
            {
                // ignore, this simply means the portlet is not cacheable
            }
            catch (Exception e)
            {
                // invocation failed or security exception, in both case
                // log the error and treat the class as non cacheable
                logger.error("PortletFactory: failed to get cache handle",e);
            }
        }
        
        try {

            if (enableCache && (handle != null))
            {
                portlet = (Portlet)PortletCache.getCacheable( handle );

                //portlet in cache but expired, remove it from cache
                if ((portlet!=null) && ((Cacheable)portlet).getExpire().isExpired() )
                {
                    logger.info( "The portlet (" + handle + ") is expired" );
                    PortletCache.removeCacheable(handle);
                    if ( logger.isDebugEnabled() )
                    {
                        logger.debug( "After removal of object(" + handle + ")." );
                    }
                    portlet = null;
                }
            }

            // we found a portlet in the cache
            if ( (portlet != null)
                 && ( portlet instanceof Cacheable )
                 && (! ((Cacheable)portlet).getExpire().isExpired()) )
            {
                // update the config for the portlet to the current one
                // Note: this is what was used to find the cached portlet.
                // Note: the init params may have changed in the psml since caching,
                //       this will update the portlet to use them.
                portlet.setPortletConfig( pc );
                portlet.setID( id );
                portlet.setName( pc.getName() );

                //FIXME: we now avoid to override metainfo when nothing is set
                //in the markup, so that cached portlets can keep their metainfo
                //This may lead to an incorrect metainfo retrieved if the first
                //instance of the portlet, which is put in the cache, has some
                //special metainfo defined in the markup 

                MetaData meta = pc.getMetainfo();
                
                if ( meta != null)
                {

                    if (! MetaData.DEFAULT_TITLE.equals( meta.getTitle() ) )
                    {
                        portlet.setTitle( meta.getTitle() );
                    }
    
                    if (! MetaData.DEFAULT_DESCRIPTION.equals( meta.getDescription() ) )
                    {
                        portlet.setDescription( meta.getDescription() );
                    }
                }

                //FIXME: Notice here we are putting the portlet without wrapper
                //in the cache, and we must wrap it on return.
                //Security implications: the portletcache should not be
                //publicly accessible.
                //Alternative: we could wrap the portlet before putting
                //it in the cache.

                //now compute the time it took to instantate and log it...
                // time in millis, sugested by Thomas Schaeck (schaeck@de.ibm.com)
                long milliseconds = ( System.currentTimeMillis() - begin );
        
                if (logger.isDebugEnabled())
                    logger.debug( "PortletFactory.getPortlet(): found in cache in "
                        + milliseconds + " ms - handle: " + handle );

                return PortletWrapper.wrap( portlet );
            }

            // if not found in the cache, instanciate a new Portlet
            portlet = (Portlet)portletClass.newInstance();

        }
        catch ( Throwable t )
        {
            logger.error("Throwable", t);
            throw new PortletException( t.getMessage() );
        }

        // save the current meta-info
        String title = null;
        String description = null;
        MetaData metainfo = pc.getMetainfo();
        
        if ( metainfo != null ) {
            title=metainfo.getTitle();
            description=metainfo.getDescription();
        }
        
        
        // init the portlet, it may override its PSML defined markup if
        // it doesn't check for it
        portlet.setID( id );
        portlet.setName( pc.getName() );
        portlet.setPortletConfig( pc );
        portlet.setCreationTime( System.currentTimeMillis() );
        portlet.init();

        //force the title and description from markup metadata
        //in case the portlet overwrote some values

        if ( metainfo != null)
        {
            if (!MetaData.DEFAULT_TITLE.equals(title) )
            {
                portlet.setTitle( title );
            }

            if (!MetaData.DEFAULT_DESCRIPTION.equals(description) )
            {
                portlet.setDescription( description );
            }
        }

        if (enableCache && (portlet instanceof Cacheable))
        {
            //place this portlet in a cache...
            ((Cacheable)portlet).setHandle( handle );
            PortletCache.addCacheable( ((Cacheable)portlet) );
            //Expiration should be added to the portlet now, so that
            //the watcher is created before file changes on disk.
            ((Cacheable)portlet).getExpire();

        }

        //now compute the time it took to instantate and log it...
        // time in millis, sugested by Thomas Schaeck (schaeck@de.ibm.com)
        long milliseconds = ( System.currentTimeMillis() - begin );

        if (logger.isDebugEnabled())
            logger.debug( "PortletFactory.getPortlet(): constructed in "
                + milliseconds + " ms - handle: " + handle );

        return PortletWrapper.wrap( portlet );

    }

    /**
     * Given a Registry Entry, get the value of what its PortletConfig would be.
     *
     * @param entry the PSML Entry containing the config
     * @param portletId the PSML entry's portlet id
     * @return the newly created PortletConfig object
     */
    protected PortletConfig getPortletConfig( PortletEntry portletEntry, String id)
    {
        Map map = new HashMap();
        map.putAll(portletEntry.getParameterMap());
        
        PortletConfig pc = new BasePortletConfig();
        pc.setName( portletEntry.getName() );
        addParentInitParameters(portletEntry, map);        
        pc.setInitParameters( map );
        pc.setMetainfo( getMetaData( portletEntry ) );
        pc.setURL( portletEntry.getURL() );
        pc.setCachedOnURL( portletEntry.isCachedOnURL() );
        //pc.setSecurityRef(portletEntry.getSecurityRef());
        pc.setSecurityRef(getSecurityReference(null, portletEntry));

        if (runDataService != null)
        {
            JetspeedRunData rundata = runDataService.getCurrentRunData();
            if (rundata != null)
            {
                Profile profile = rundata.getProfile();
                if (profile != null)
                {
                    pc.setPageId(profile.getId());
                }
            }
        }
        pc.setPortletId(id);

        return pc;
    }
    
    
    /**
     * Fetches the parameters out of a PSML Entry
     * 
     * @param entry the Entry to check for parameters
     * @return a Map containing the parameters names/values, an empty Map 
     *         is returned if there are no parameters
     */
    protected static Map getParameters( Entry entry )
    {
        Hashtable hash = new Hashtable();
        
        Parameter[] props = entry.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(Entry entry)
    {
        MetaData data = new MetaData();
        MetaInfo meta = entry.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() );
        }

        if ( entry.getParent() != null )
        {

            PortletEntry parent = (PortletEntry)Registry
                .getEntry( Registry.PORTLET, entry.getParent() );

            if (parent != null)
            {
                MetaData parentData = getMetaData( parent );
                parentData.merge(data);
                return parentData;
            }
            
        }

        return data;

    }

    /**
    Create a MetaData object from a registry Metainfo object
    
    @param meta the Metainfo to copy

    @return the new MetaData object, empty if meta is null
    */
    protected static MetaData getMetaData(PortletEntry entry)
    {
        MetaData data = new MetaData();

        if ( entry.getTitle() != null )
            data.setTitle( entry.getTitle() );

        if ( entry.getDescription() != null )
            data.setDescription( entry.getDescription() );
            
		if ( entry.getMetaInfo() != null && entry.getMetaInfo().getImage() != null )
			data.setImage( entry.getMetaInfo().getImage() );
            
        return data;
    }
    
    /**
     * @param Entry entry Entry whose parent we want
     * @return PortletEntry Parent of Entry
     * @author <a href="mailto:weaver@apache.org">Scott T. Weaver</a>
     */
    protected static PortletEntry getParentEntry(PortletEntry entry)
    {
        PortletEntry result = null;
        String parent = entry.getParent();
        if (parent != null)
        {
            result = (PortletEntry) Registry.getEntry(Registry.PORTLET, parent);
        }

        return result;
    }
    
    /**
     * Retruns the classname defined for this PortletEntry.
     * If no classname was defined, the parent is queried
     * @author <a href="mailto:weaver@apache.org">Scott T. Weaver</a>
     */
    protected String getClassname(PortletEntry entry)
    {
        String className = entry.getClassname();
        if (className == null)
        {
            PortletEntry parent = getParentEntry(entry);
            if (parent != null)
            {
            	// We must walk up the hierarchy just to be safe
                className = getClassname(parent);
            }
        }

        return className;
    }
    
    /**
     * Maps all parameters, not found within the <code>entry</code>, from
     * the <code>entry</code>'s parent into the entry
     * @author <a href="mailto:weaver@apache.org">Scott T. Weaver</a>
     */
    protected void addParentInitParameters(PortletEntry entry, Map hash)
    {
        // Now map any parameters from the parent that the child does not have
        PortletEntry parent = getParentEntry(entry);
        if (parent != null)
        {
            Map parentMap = parent.getParameterMap();
            Iterator names = parent.getParameterNames();

            while (names.hasNext())
            {
                String key = (String) names.next();
                if (!hash.containsKey(key))
                {
                    hash.put(key, parentMap.get(key));                    
                }
            }
            
            // Always make sure to get the entire inheritence chain
           addParentInitParameters(parent, hash);
        }        
    }
    
    /**
     * Figures out how to produce a security reference for
     * this portlet.
     */
    protected SecurityReference getSecurityReference(Entry entry, PortletEntry pEntry)
    {
        // If something happended during init() that prevented this
        if (runDataService == null)
        {
            this.runDataService =
                (JetspeedRunDataService) TurbineServices.getInstance().getService(
                    RunDataService.SERVICE_NAME);
        }
        JetspeedRunData rundata = runDataService.getCurrentRunData();
        
        return JetspeedSecurity.getSecurityReference(entry,  rundata);
    }

}
