/*
 * 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.portal.portlets;

//jetspeed
import org.apache.jetspeed.capability.CapabilityMap;
import org.apache.jetspeed.capability.CapabilityMapFactory;
import org.apache.jetspeed.om.registry.MediaTypeEntry;
import org.apache.jetspeed.om.registry.PortletEntry;
import org.apache.jetspeed.portal.BasePortletConfig;
import org.apache.jetspeed.portal.expire.Expire;
import org.apache.jetspeed.portal.expire.ExpireFactory;
import org.apache.jetspeed.portal.Portlet;
import org.apache.jetspeed.portal.PortletConfig;
import org.apache.jetspeed.portal.PortletException;
import org.apache.jetspeed.portal.PortletState;
import org.apache.jetspeed.services.persistence.PersistenceManager;
import org.apache.jetspeed.services.persistence.PortalPersistenceException;
import org.apache.jetspeed.portal.PortletInstance;
import org.apache.jetspeed.services.portletcache.Cacheable;
import org.apache.jetspeed.services.Registry;
import org.apache.jetspeed.services.logging.JetspeedLogFactoryService;
import org.apache.jetspeed.services.logging.JetspeedLogger;
import org.apache.jetspeed.util.JetspeedException;
import org.apache.jetspeed.util.MetaData;
import org.apache.jetspeed.util.MimeType;

//ecs
import org.apache.jetspeed.util.JetspeedClearElement;
import org.apache.ecs.ConcreteElement;

//turbine stuff
import org.apache.turbine.services.cache.CachedObject;
import org.apache.turbine.services.cache.Refreshable;
import org.apache.turbine.util.RunData;

//java stuff
import java.util.Hashtable;
import java.util.Iterator;
/**
<p>
Should be used by most Portlets that wish to conform to default behavior
</p>

<p>
PERFORMANCE NOTE:

getContent returns a StringElement that was generated on setContent().  This is
used so that performance is increased since ECS does not have to work overtime
to generate output.
</p>

@author <A HREF="mailto:burton@apache.org">Kevin A. Burton</A>
@author <A HREF="mailto:raphael@apache.org">Rapha謖 Luta</A>
@author <A HREF="mailto:sgala@apache.org">Santiago Gala</A>
@author <A HREF="mailto:paulsp@apache.org">Paul Spencer</A>
@author <A HREF="mailto:morciuch@apache.org">Mark Orciuch</A>
@version $Id: AbstractPortlet.java,v 1.65 2004/03/29 21:38:42 taylor Exp $
*/
public abstract class AbstractPortlet implements Portlet, PortletState, Cacheable, Refreshable
{

    /**
     * Static initialization of the logger for this class
     */    
    private static final JetspeedLogger logger = JetspeedLogFactoryService.getLogger(AbstractPortlet.class.getName());    
    
    private boolean         cacheable = true;
    private PortletConfig   pc = null;

    /**
    Provide a required name for this Portlet
    */
    private String        name            = null;

    /**
    Provide a Unique Portlet ID
    */
    private String id = null;

    /**
    Cache handle for this object.
    */
    private String        handle          = "";

    /**
    Expiration time of object in milliseconds since the standard base time
    known as "the epoch", namely January 1, 1970, 00:00:00 GMT.
    */
    private Long          expirationMillis = null;

    /**
    Holds instances of ConcreteElements (Portlet output/content)
    based on its current CapabilityMap.
    */
    protected Hashtable     content         = new Hashtable();

    /**
    The time this portlet was created.
    */
    private long creationTime;

    /**
     * Handle to cached object
     */
    private CachedObject cachedObject = null;

    /**
    */
    protected void clearContent() {
        this.content.clear();
    }

    /**
     */
    protected void setContent( ConcreteElement content ) {
        this.setContent( content,
                         CapabilityMapFactory.getDefaultCapabilityMap() );
    }

    /**
    */
    protected void setContent( String content ) {
        this.setContent( new JetspeedClearElement( content ),
                         CapabilityMapFactory.getDefaultCapabilityMap() );
    }

    /**
    */
    protected void setContent( ConcreteElement content,
                               CapabilityMap map )
                          throws IllegalArgumentException
    {
        CapabilityMap mymap = map;
        if ( mymap == null ) {
            mymap = CapabilityMapFactory.getDefaultCapabilityMap();
        }

        ConcreteElement buffer = new JetspeedClearElement( content.toString( ) );
        this.content.put( mymap.toString(), buffer );
    }


    /*
     * Implement methods required by Refreshable
     */

    /**
     * Usually called by caching system when portlet is marked as expired, but
     * has not be idle longer then TimeToLive.
     *
     * This method should be implement in cachable portlets
     */
    public void refresh() {
        /*
         * The following message is here to add in debugging.  It is
         * expected the any portlet type that is refreshable will
         * implement this method.
         */
        logger.debug( "AbstractPortlet - Refreshing " + this.getName() );
    }

    /*
     * Implement methods required by Cacheable
     */

    /**
     * Is this portlet cacheable.  It is the portlet's responsability to
     * cache the content.
     *
     * @return <CODE>true</CODE> Cachable<BR>
     * <CODE>false</CODE> Not cachable
     */
    public boolean isCacheable() {
        return this.cacheable;
    }

    /**
     * Set cachable.  This should only be called in the portlet's init().
     *
     * @param cacheable <CODE>true</CODE> Portlet is cachable<BR>
     * <CODE>false</CODE> Portlet is NOT cachable
     */
    public void setCacheable(boolean cacheable) {
        this.cacheable = cacheable;
    }


    /**
     * Used by a Cacheable object to determine when it should expire itself from the cache.
     *
     * @return Expire
     */
    public Expire getExpire() {
        try {
            return ExpireFactory.getExpire( this, ExpireFactory.NO_EXPIRE );
        } catch ( JetspeedException e ) {
            logger.error("Exception",  e);
            return null;
        }
    }

    /**
     * <p>Used by the cache to get a unique reference on what you want to add
     * and then retrieve in the future from the cache</p>
     *
     * <p>Most implementations should just call the CacheHandleManager with
     * the given params within the implementation and just return this.</p>
     *
     * @return Cache handle (key)
     */
    public final String getHandle() {
        return this.handle;
    }

    /**
     * Used by a Cacheable object to determine when it should
     * expire itself from the cache.
     *
     * @param handle Cache Handle
     *
     * @deprecated cacheable classes should now implement a static getHandle(config) method
     */
    public final void setHandle( String handle ) {
        this.handle = handle;
    }

    /**
     * Set the expiration time in milliseconds.
     *
     * @return Expiration time in milliseconds since epoch, or null if the expiration was not set.
     */
    public Long getExpirationMillis() {
      return this.expirationMillis;
    }

    /**
     * Sets the cache expiration time.  When the portlet is stale (expired),
     * the refresh() will be called if the portlet has not been untouched
     * longer then then it's TimeToLive.
     *
     * @param expirationMillis setExpirationMillis Expiration in milliseconds since epoch
     */
    public void setExpirationMillis( long expirationMillis) {
      this.expirationMillis = new Long(expirationMillis);

      if (cachedObject != null)  {
          long expirationInterval = this.expirationMillis.longValue() - cachedObject.getCreated();
          if (expirationInterval > 0) {
              cachedObject.setExpires(expirationInterval);
          } else {
              cachedObject.setStale(true);
          }
      }
    }

    /**
     * Builds a new cache handle for this cacheable class with the specified
     * config object.
     *
     * @param config The configuration object to use for building the handle
     *
     * @return A cache handle
     */
    public static Object getHandle(Object config)
    {
        //this implementation expects a PortletConfig object as its
        // configuration
        PortletConfig pc = null;

        if (!(config instanceof PortletConfig))
        {
            return null;

        }

        // By default, only take into account the init parameters
        pc = (PortletConfig)config;
        StringBuffer handle = new StringBuffer(256);

        if (pc.getURL()!=null && pc.isCachedOnURL())
        {
            handle.append(String.valueOf(pc.getURL().hashCode()));
        }

        Iterator i = pc.getInitParameterNames();
        while(i.hasNext())
        {
            String name = (String)i.next();
            String value = pc.getInitParameter(name);

            if (value!=null)
            {
                handle.append("|").append(name).append("-").append(value);
            }
        }

        return handle.toString();
    }

    /**
     * Set this portlet's cached object.
     *
     * @param cachedObject Cached Object associated to this portlet
     */
     public void setCachedObject(CachedObject cachedObject) {
        this.cachedObject = cachedObject;
    }

    /*
     * Implement methods required by Portlet
     */

    /**
     * Get the portlet's name
     *
     * @return Name of the portlet
     */
    public String getName()
    {

        if ( name == null )
        {
            if (getPortletConfig()!=null)
            {
                if (getPortletConfig().getName()!=null)
                {
                    return getPortletConfig().getName();
                }
                else
                {
                    return this.getClass().getName();
                }
            }
        }

        return name;

    }

    /**
     * Set the name of the portlet
     *
     * @param name Name of the portlet
     */
    public void setName( String name ) {
        this.name = name;
    }

    /**
     * Get the config of this servlet
     *
     * @return PortletConfig Portlet
     */
    public PortletConfig getPortletConfig() {
        return this.pc;
    }

    /**
     * Set's the configuration of this servlet.
     */
    public void setPortletConfig( PortletConfig pc ) {
        this.pc = pc;
    }

    /**
     * @param rundata The RunData object for the current request
     */
    public ConcreteElement getContent( RunData rundata ) {

        return getContent( rundata, null , true );
    }

    public ConcreteElement getContent( RunData rundata, CapabilityMap map ) {
        CapabilityMap mymap = map;
        if ( mymap == null ) mymap = CapabilityMapFactory.getCapabilityMap( rundata );

        return (ConcreteElement)content.get( mymap.toString() );
    }

    /**
     * @param rundata The RunData object for the current request
     */
    public ConcreteElement getContent( RunData rundata,
                                       CapabilityMap map,
                                       boolean allowRecurse ) {

        CapabilityMap mymap = map;
        if ( mymap == null ) mymap = CapabilityMapFactory.getCapabilityMap( rundata );

        ConcreteElement element = (ConcreteElement)content.get( mymap.toString() );

        if ( element == null ) {
            if ( allowRecurse ) {
                try {
                    // init will put content under default cmap
                    init( );
                    element = getContent( rundata, mymap, false );
                    if( element != null ) {
                        // now we put it under our cmap
                        this.setContent( element, mymap );
                    }
                } catch (Exception e) {
                    element = new JetspeedClearElement("Error when retrieving Portlet contents");
                    if( logger.isDebugEnabled() ) {
                        logger.debug( "Error when retrieving Portlet contents", e );
                    }
                }
            } else {
                if( element == null ) {
                    //FIXME: Let's asume that the contents under "default" map are good
                    mymap = CapabilityMapFactory.getDefaultCapabilityMap();
                    element = (ConcreteElement)content.get( mymap.toString() );
                    if( element == null ) {
                        element = new JetspeedClearElement("Unknown Problem getting Contents");
                    }
                }
            }
        }

        return element;

    }

    /**
     * Provide a description within PML if the user has specified one.
     *
     * @return a null entry if the user hasn't defined anything
     */
    public String getDescription() {
        if (getPortletConfig()!=null)
            if (getPortletConfig().getMetainfo()!=null)
                return getPortletConfig().getMetainfo().getDescription();

        return null;
    }

    /**
     * Provide a Description within PML if the user has specified one.
     *
     * @return a null if entry AND portlet have not defined a description
     */
    public String getDescription(String instanceDescription)
    {
      if (instanceDescription != null)
          return instanceDescription;
      return getDescription();
    }

    /**
     */
    public void setDescription( String description ) {
        PortletConfig pc = getPortletConfig();
        if (pc==null) {
            pc = new BasePortletConfig();
            setPortletConfig(pc);
        }

        MetaData meta = pc.getMetainfo();
        if (meta==null) {
            meta = new MetaData();
            pc.setMetainfo(meta);
        }

        meta.setDescription(description);
    }

    //provide default titles so that the user can define them in PML

    /**
     * Provide a title within PML if the user has specified one.
     *
     * @return a null entry if the user hasn't defined anything
     */
    public String getTitle()
    {
        if (getPortletConfig()!=null)
            if (getPortletConfig().getMetainfo()!=null)
                return getPortletConfig().getMetainfo().getTitle();

        return null;
    }

    /**
     * Provide a title within PML if the user has specified one.
     *
     * @return a null if entry AND portlet have not defined a title
     */
    public String getTitle(String instanceTitle)
    {
      if (instanceTitle != null)
          return instanceTitle;
      return getTitle();
    }

    /**
     * Set the title for this Portlet.
     * @param title Portlet title.
     */
    public void setTitle( String title ) {
        PortletConfig pc = getPortletConfig();
        if (pc==null) {
            pc = new BasePortletConfig();
            setPortletConfig(pc);
        }

        MetaData meta = pc.getMetainfo();
        if (meta==null) {
            meta = new MetaData();
            pc.setMetainfo(meta);
        }

        meta.setTitle(title);
    }


    /**
     * Getter for property image.
     * @return Name of portlet image, icon.  The name is expected to be in the form of a URL.
     */
    public String getImage()
    {
        if (getPortletConfig()!=null)
            if (getPortletConfig().getMetainfo()!=null)
                return getPortletConfig().getMetainfo().getImage();

        return null;
    }

    /**
     * Getter for property image.
     * @return a null if entry AND portlet have not defined an icon.
     */
    public String getImage(String instanceImage)
    {
      if (instanceImage != null)
          return instanceImage;
      return getImage();
    }

    public void setImage( String image )
    {
        PortletConfig pc = getPortletConfig();
        if (pc==null) {
            pc = new BasePortletConfig();
            setPortletConfig(pc);
        }

        MetaData meta = pc.getMetainfo();
        if (meta==null) {
            meta = new MetaData();
            pc.setMetainfo(meta);
        }

        meta.setImage(image);
    }

    /**
     * Is the portled editable/customizeable.
     * @param rundata The RunData object for the current request
     * @return <CODE>true</CODE> Editing is allow
     * <CODE>false</CODE> Editing is NOT alowed
     */
    public boolean getAllowEdit( RunData rundata )
    {
        return allowCustomize(rundata);
    }

    /**
     * Is the portled viewable.
     * @param rundata The RunData object for the current request
     * @return <CODE>true</CODE> Viewing is allow
     * <CODE>false</CODE> Viewing is NOT alowed
     * 
     * Override this method to control your own View behavior
     */
    public boolean getAllowView( RunData rundata )
    {
        return allowView( rundata );
    }
    
    /**
     * Can this portlet be maximized
     * @param rundata The RunData object for the current request
     * @return <CODE>true</CODE> Portlet can be maximized<br>
     * <CODE>false</CODE> Portlet can NOT be maximized
     */
    public boolean getAllowMaximize( RunData rundata )
    {
        return allowMaximize( rundata );
    }

    /**
     * By default don't provide any initialization
     */
    public void init( ) throws PortletException
    {
        // make sure to clean all content
        clearContent();
    }

    /**
     */
    public long getCreationTime() {
        return this.creationTime;
    }

    /**
     */
    public void setCreationTime( long creationTime ) {
        this.creationTime = creationTime;
    }

    /**
     */
    public boolean supportsType( MimeType mimeType )
    {
        PortletEntry entry = (PortletEntry)Registry.getEntry(Registry.PORTLET, getName() );
        String baseType = mimeType.toString();
        if (entry!=null)
        {
            Iterator i = entry.listMediaTypes();

            while(i.hasNext())
            {
                String name = (String)i.next();
                MediaTypeEntry media = (MediaTypeEntry)Registry.getEntry(Registry.MEDIA_TYPE, name);
                if (media != null)
                {
                    if (baseType.equals(media.getMimeType())) return true;
                }
            }
        }

        return MimeType.HTML.equals( mimeType );
    }

    /*
     * Implement methods required by PortletState
     */

    /**
     * Implements the default close behavior:
     * security permissions will be checked.
     *
     * @param rundata The RunData object for the current request
     */
    public boolean allowClose( RunData rundata )
    {
        //Security will not allow this call to succeed if there are
        //not enough permissions
        return !isClosed( rundata );
    }

    /**
     * Returns true if this portlet is currently closed
     *
     * @param rundata The RunData object for the current request
     */
    public boolean isClosed(RunData rundata)
    {
        return this.getAttribute("_display", "normal", rundata ).equals("closed");
    }

    /**
     * Toggles the portlet state between closed and normal
     *
     * @param minimized the new portlet state
     * @param rundata The RunData object for the current request
     */
    public void setClosed(boolean close, RunData rundata)
    {
        if( allowClose( rundata ) )
        {
            this.setAttribute("_display", close ? "closed" : "normal", rundata );
        }
    }

    /**
     * Implements the default info behavior:
     * security permissions will be checked.
     *
     * @param rundata The RunData object for the current request
     */
    public boolean allowInfo( RunData rundata )
    {
        //Security will not allow this call to succeed if there are
        //not enough permissions
        return true;
    }

    /**
     * Implements the default customize behavior:
     * security permissions will be checked.
     *
     * @param rundata The RunData object for the current request
     */
    public boolean allowCustomize( RunData rundata )
    {
        //Security will not allow this call to succeed if there are
        //not enough permissions
        return true;
    }

    /**
     * Implements the default maximize behavior:
     * security permissions will be checked.
     *
     * @param rundata The RunData object for the current request
     */
    public boolean allowMaximize( RunData rundata )
    {
        //Security will not allow this call to succeed if there are
        //not enough permissions
        return true;
    }

    /**
     * Implements the default info behavior:
     * security permissions will be checked.
     *
     * @param rundata The RunData object for the current request
     */
    public boolean allowMinimize( RunData rundata )
    {
        //Security will not allow this call to succeed if there are
        //not enough permissions
        return true;
    }

    /**
     * Implements the default view behavior:
     * security permissions will be checked.
     *
     * @param rundata The RunData object for the current request
     */
    public boolean allowView( RunData rundata )
    {
        //Security will not allow this call to succeed if there are
        //not enough permissions
        return true;
    }
    
    /**
     * Implements the default print friendly format behavior:
     * security permissions will be checked.
     *
     * @param rundata The RunData object for the current request
     */
    public boolean allowPrintFriendly( RunData rundata )
    {
        //Security will not allow this call to succeed if there are
        //not enough permissions
        return true;
    }

    /**
     * Returns true if this portlet is currently minimized
     *
     * @param rundata The RunData object for the current request
     */
    public boolean isMinimized(RunData rundata)
    {
        return this.getAttribute("_display", "normal", rundata ).equals("minimized");
    }

    /**
     * Change the portlet visibility state ( minimized <-> normal )
     *
     * @param minimize True if the portlet change to minimized
     * @param rundata The RunData object for the current request
     */
    public void setMinimized( boolean minimize, RunData rundata )
    {
        if( allowMinimize( rundata ) )
        {
            this.setAttribute("_display", minimize ? "minimized" : "normal", rundata );
        }
    }

    /**
     * Returns TRUE if the title bar in should be displayed. The title bar includes
     * the portlet title and action buttons.  This
     *
     * @param rundata The RunData object for the current request
     */
    public boolean isShowTitleBar(RunData rundata)
    {
        if (getPortletConfig()!=null)
        {
            // Parameter can exist in PSML or <portlet-entry>
            return Boolean.valueOf(getPortletConfig().getInitParameter("_showtitlebar","true")).booleanValue();
        }
        return this.getAttribute("_showtitlebar", "true", rundata ).equals("true");
    }
    // utility methods

    /**
     * Retrieve a portlet attribute from persistent storage
     *
     * @param attrName The attribute to retrieve
     * @param attrDefValue The value if the attr doesn't exists
     * @param rundata The RunData object for the current request
     * @return The attribute value
     */
    public String getAttribute( String attrName, String attrDefValue, RunData rundata )
    {
        String attrValue = null ;

        PortletInstance instance = PersistenceManager.getInstance(this, rundata);
        attrValue = instance.getAttribute(attrName, attrDefValue);

        return attrValue;
    }

    /**
     * Stores a portlet attribute in persistent storage
     *
     * @param attrName The attribute to retrieve
     * @paarm attrValue The value to store
     * @param rundata The RunData object for the current request
     */
    public void setAttribute( String attrName, String attrValue, RunData rundata )
    {
        try
        {
            PortletInstance instance = PersistenceManager.getInstance(this, rundata);
            instance.setAttribute(attrName, attrValue);
            PersistenceManager.store(instance);
        }
        catch (PortalPersistenceException e)
        {
            logger.error("Exception while setting attribute "+attrName+" for portlet "+getName(), e);
        }
    }

    /**
     * Gets the portlet instance associated with this portlet.
     *
     * @param rundata The RunData object for the current request
     * @return PortletInstance
     */
    public PortletInstance getInstance(RunData rundata)
    {
        return PersistenceManager.getInstance(this, rundata);
    }

    //
    // DST: Shouldn't getID and setID be deprecated and added to PortletInstance...
    //
    public String getID()
    {
        return id;
    }

    public void setID(String id)
    {
        this.id = id;
    }

    /**
    * @return true if the portlet does its own customization
    */
    public boolean providesCustomization()
    {
        return false;
    }

}
