/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.aggregator.impl;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

import javax.portlet.PortletMode;
import javax.portlet.WindowState;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.jetspeed.JetspeedActions;
import org.apache.jetspeed.PortalReservedParameters;
import org.apache.jetspeed.aggregator.PortletAccessDeniedException;
import org.apache.jetspeed.aggregator.PortletContent;
import org.apache.jetspeed.aggregator.PortletRenderer;
import org.apache.jetspeed.aggregator.PortletTrackingManager;
import org.apache.jetspeed.aggregator.RenderingJob;
import org.apache.jetspeed.aggregator.WorkerMonitor;
import org.apache.jetspeed.cache.CacheElement;
import org.apache.jetspeed.cache.ContentCacheKey;
import org.apache.jetspeed.cache.JetspeedCache;
import org.apache.jetspeed.container.PortletWindow;
import org.apache.jetspeed.om.page.ContentFragment;
import org.apache.jetspeed.om.page.Fragment;
import org.apache.jetspeed.om.portlet.LocalizedField;
import org.apache.jetspeed.om.portlet.PortletDefinition;
import org.apache.jetspeed.request.RequestContext;
import org.apache.jetspeed.security.SecurityAccessController;
import org.apache.jetspeed.statistics.PortalStatistics;
import org.apache.pluto.container.PortletContainer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * <h4>PortletRendererService <br />
 * Jetspeed-2 Rendering service.</h4>
 * <p>
 * This service process all portlet rendering requests and interfaces with the
 * portlet container to generate the resulting markup
 * </p>
 * 
 * @author <a href="mailto:raphael@apache.org">Rapha�l Luta </a>
 * @author <a href="mailto:taylor@apache.org">David Sean Taylor</a>
 * @author <a>Woonsan Ko</a>
 * @version $Id: PortletRendererImpl.java,v 1.30 2005/05/20 14:54:22 ate Exp $
 */
public class PortletRendererImpl implements PortletRenderer
{
    protected final static Logger log = LoggerFactory.getLogger(PortletRendererImpl.class);

    protected WorkerMonitor workMonitor;
    protected PortletContainer container;
    protected PortalStatistics statistics;

    protected PortletTrackingManager portletTracking;
    
    /**
     *  flag indicating whether to check jetspeed-portlet.xml security constraints 
     *  before rendering a portlet. If security check fails, do not display portlet content
     */
    protected boolean checkSecurityConstraints;   
    /**
     * For security constraint checks
     */
    protected SecurityAccessController accessController;
    
    /**
     * JSR 168 Portlet Content Cache
     */
    protected JetspeedCache portletContentCache;
    
    /**
     * The default OutOfService message
     */
    public static final String DEFAULT_OUT_OF_SERVICE_MESSAGE = "Portlet is not responding and has been taken out of service.";
    
    /**
     * The OutOfService message
     */
    protected String outOfServiceMessage = DEFAULT_OUT_OF_SERVICE_MESSAGE;
    
    public PortletRendererImpl(PortletContainer container, 
                               WorkerMonitor workMonitor,
                               PortalStatistics statistics,
                               PortletTrackingManager portletTracking,
                               boolean checkSecurityConstraints,
                               SecurityAccessController accessController,
                               JetspeedCache portletContentCache)
    {
        this.container = container;
        this.workMonitor = workMonitor;
        this.statistics = statistics;
        this.portletTracking = portletTracking;
        this.checkSecurityConstraints = checkSecurityConstraints;
        this.accessController = accessController;
        this.portletContentCache = portletContentCache;
    }

    public PortletRendererImpl(PortletContainer container, 
                               WorkerMonitor workMonitor,
                               PortalStatistics statistics)
    {
        this(container, workMonitor, statistics, null, false, null, null);
    }

    public PortletRendererImpl(PortletContainer container, 
                               WorkerMonitor workMonitor)
    {
        this( container, workMonitor, null );
    }
    
    public void start()
    {
        // workMonitor.start();
    }

    public void stop()
    {
        // this.monitor.shutdown ?
    }
    
    public void setOutOfServiceMessage(String outOfServiceMessage)
    {
        this.outOfServiceMessage = outOfServiceMessage;
    }
    
    /**
     * Render the specified Page fragment. Result is returned in the
     * PortletResponse.
     */
    public void renderNow( ContentFragment fragment, RequestContext requestContext )
    {
        renderNow(fragment, requestContext, false);
    }
    /**
     * Render the specified Page fragment. Result is returned in the
     * PortletResponse.
     */
    public void renderNow( ContentFragment fragment, RequestContext requestContext, boolean spawned )
    {
        try
        {
            RenderingJob rJob = createRenderingJob(fragment, requestContext);
            if (rJob != null)
            {
                if (spawned)
                {
                    ArrayList<RenderingJob> jobs = new ArrayList<RenderingJob>();
                    jobs.add(rJob);
                    processRenderingJob(rJob, true);
                    waitForRenderingJobs(jobs);
                }
                else
                {
                    rJob.execute();
                }
            }
        }
        catch (PortletAccessDeniedException e)
        {
            fragment.overrideRenderedContent(e.getLocalizedMessage());                        
        }        
        catch (Exception e)
        {
            fragment.overrideRenderedContent(e.getLocalizedMessage());
            log.error(e.getMessage(),e);
        }
    }

    /**
     * Render the specified Page fragment. Result is returned in the
     * PortletResponse.
     */
    public void renderNow( ContentFragment fragment, HttpServletRequest request, HttpServletResponse response )
    {
        RequestContext requestContext = (RequestContext) request
                .getAttribute(PortalReservedParameters.REQUEST_CONTEXT_ATTRIBUTE);
        renderNow(fragment, requestContext);
    }
    
    protected int getExpirationCache(PortletDefinition portletDefinition)
    {
        return portletDefinition == null ? 0 : portletDefinition.getExpirationCache();
    }
    
    /** 
     * 
     * Create a rendering job for the specified Page fragment.
     * The method returns a rendering job which should be passed to 'processRenderingJob(RenderingJob job)' method.
     * @return portlet rendering job to pass to render(RenderingJob job) method
     * @throws RuntimeException
     */
    public RenderingJob createRenderingJob(ContentFragment fragment, RequestContext requestContext)
    throws PortletAccessDeniedException
    {
        RenderingJob job = null;
        try
        {
            PortletWindow portletWindow = requestContext.getPortletWindow(fragment);
            if (!portletWindow.isValid())
            {
                return null;
            }
            PortletDefinition portletDefinition = portletWindow.getPortletDefinition();     

            long timeoutMetadata = this.getTimeoutOnJob(portletDefinition);
            portletTracking.setExpiration(portletWindow, timeoutMetadata);            
            
            if ((checkSecurityConstraints || this.enforceSecurityConstraint(portletDefinition)) && 
                !checkSecurityConstraint(portletDefinition, fragment))
            {
                throw new PortletAccessDeniedException("Access Denied.");
            }
            if (portletTracking.isOutOfService(portletWindow))
            {
                log.info("Taking portlet out of service: " + portletDefinition.getUniqueName() + " for window " + portletWindow.getId());
                fragment.overrideRenderedContent(outOfServiceMessage);
                return null;
            }
            int expirationCache = getExpirationCache(portletDefinition);
            if (expirationCache != 0)
            {
                portletTracking.setExpiration(portletWindow, expirationCache);
                if (retrieveCachedContent(requestContext, portletWindow, expirationCache, portletDefinition))
                {
                    return null;
                }
            }
            job = buildRenderingJob( portletWindow, requestContext, true, portletDefinition, timeoutMetadata );
        }
        catch (PortletAccessDeniedException pade)
        {
            throw pade;
        }
        catch (Exception e)
        {
            throw new RuntimeException("Failed to create rendering job", e);
        }

        return job;
    }
           
    /** 
     * 
     * Render the specified rendering job.
     * The method returns before rendering is complete when the job is processed in parallel mode.
     * When it is not parallel mode, it returns after rendering is complete.
     */
    public void processRenderingJob(RenderingJob job)
    {
        processRenderingJob(job, false);
    }

    protected void processRenderingJob(RenderingJob job, boolean parallelOnly)
    {
        try
        {
            if (parallelOnly || job.getTimeout() > 0)
            {
                workMonitor.process(job);
            }
            else
            {
                job.execute();
            }
        }
        catch (Exception e1)
        {
            log.error("render() failed: " + e1.toString(), e1);
            job.getFragment().overrideRenderedContent(e1.getLocalizedMessage());            
        }
    }
    
    /**
     * Wait for all rendering jobs in the collection to finish successfully or otherwise. 
     * @param renderingJobs the Collection of rendering job objects to wait for.
     */
    public void waitForRenderingJobs(List<RenderingJob> renderingJobs)
    {
        this.workMonitor.waitForRenderingJobs(renderingJobs);
    }

    /**
     * Retrieve cached content, if content retrieved successfully return true, if no content found return false
     * @param requestContext
     * @param fragment
     * @param portletWindow
     * @return true when content found, otherwise false
     */
    protected boolean retrieveCachedContent(RequestContext requestContext, 
                                            PortletWindow portletWindow, int expiration, 
                                            PortletDefinition portletDefinition)
        throws Exception
    {
        ContentFragment fragment = portletWindow.getFragment();
        ContentCacheKey cacheKey = portletContentCache.createCacheKey(requestContext, fragment.getId());        
        CacheElement cachedElement = portletContentCache.get(cacheKey);
        
        if (cachedElement != null)
        {
            PortletContent portletContent = (PortletContent) cachedElement.getContent();
            
            PortletMode portletMode = portletContent.getPortletMode();
            WindowState windowState = portletContent.getWindowState();
            
            if (portletWindow.getPortletMode().equals(portletMode) && portletWindow.getWindowState().equals(windowState))
            {
                fragment.setPortletContent(portletContent);
                return true;
            }
        }
        
        return false;
    }
    
    protected PortletContent createPortletContent(RequestContext request, PortletWindow portletWindow, int expirationCache)
    {
        ContentCacheKey cacheKey = portletContentCache.createCacheKey(request, portletWindow.getFragment().getId());
        String title = portletWindow.getFragment().getTitle();            
        if (title == null)
        {
            title = request.getPreferedLanguage(portletWindow.getPortletDefinition()).getTitle();
        }
        if (title == null)
        {
            title = portletWindow.getPortletDefinition().getPortletName();
        }
        return new PortletContentImpl(cacheKey, expirationCache, title, portletWindow.getPortletMode(), portletWindow.getWindowState());
    }
    
    protected RenderingJob buildRenderingJob( PortletWindow portletWindow, 
                                              RequestContext requestContext, boolean isParallel,
                                              PortletDefinition portletDefinition, 
                                              long timeoutMetadata)
    {
        int expirationCache = getExpirationCache(portletDefinition);
        PortletContent portletContent= createPortletContent(requestContext, portletWindow, expirationCache);

        RenderingJob rJob = null;
               
        portletWindow.getFragment().setPortletContent(portletContent);
        
        rJob = new RenderingJobImpl(container, this, portletDefinition,
                                    requestContext.getRequest(), requestContext.getResponse(), requestContext, portletWindow, 
                                    statistics, expirationCache);
        
        if (isParallel)
        {
            setTimeoutOnJob(timeoutMetadata, rJob);
        }
        
        return rJob;
    }
 
    protected long getTimeoutOnJob(PortletDefinition portletDefinition)
    {
        long timeoutMetadata = 0;
        Collection<LocalizedField> timeoutFields = null;

        if (portletDefinition != null)
        {
            timeoutFields = portletDefinition.getMetadata().getFields(PortalReservedParameters.PORTLET_EXTENDED_DESCRIPTOR_RENDER_TIMEOUT);
        }

        if (timeoutFields != null && !timeoutFields.isEmpty()) 
        {
            try 
            {
                timeoutMetadata = Long.parseLong(timeoutFields.iterator().next().getValue());
            }
            catch (NumberFormatException nfe) 
            {
                log.warn("Invalid timeout metadata: " + nfe.getMessage());
            }
        }       
        return timeoutMetadata;
    }
    
    protected void setTimeoutOnJob(long timeoutMetadata, RenderingJob rJob)
    {
        
        if (timeoutMetadata > 0) 
        {
            rJob.setTimeout(timeoutMetadata);
        }
        else if (this.portletTracking.getDefaultPortletTimeout() > 0) 
        {
            rJob.setTimeout(this.portletTracking.getDefaultPortletTimeout());
        }        
    }
    
    protected boolean enforceSecurityConstraint(PortletDefinition portlet)
    {
        Collection c = portlet.getMetadata().getFields("render-time.security-constraints");
        if (c != null) 
        {
            Iterator it = c.iterator();
            if (it.hasNext()) 
            {
                LocalizedField field = (LocalizedField) it.next();
                return Boolean.parseBoolean(field.getValue());
            }
        }
        return false;
    }
    
    protected boolean checkSecurityConstraint(PortletDefinition portlet, ContentFragment fragment)
    {
        if (fragment.getType().equals(Fragment.PORTLET))
        {
            if (accessController != null)
            {
                return accessController.checkPortletAccess(portlet, JetspeedActions.MASK_VIEW);
            }
        }
        return true;
    }
 
    protected void addToCache(RequestContext context, PortletContent content)
    {
        CacheElement cachedElement = portletContentCache.createElement(content.getCacheKey(), content);
        if (content.getExpiration() == -1)
        {
            cachedElement.setTimeToIdleSeconds(portletContentCache.getTimeToIdleSeconds());
            cachedElement.setTimeToLiveSeconds(portletContentCache.getTimeToLiveSeconds());
        }
        else
        {       
            cachedElement.setTimeToIdleSeconds(content.getExpiration());
            cachedElement.setTimeToLiveSeconds(content.getExpiration());
        }
        portletContentCache.put(cachedElement);
        context.getPortalURL().getNavigationalState().registerPortletContentCachedForPublicRenderParameters(context, content);
    }    
    
    public void notifyContentComplete(RequestContext context, PortletWindow window)
    {
        PortletContent content = window.getFragment().getPortletContent();
        if (content.getExpiration() != 0)
        {
            addToCache(context, content);
        }
    }
    
    public PortletTrackingManager getPortletTrackingManager()
    {
        return this.portletTracking;
    }
}
