/*
 * 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.portals.bridges.frameworks;

import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.Iterator;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.StringTokenizer;

import javax.portlet.ActionRequest;
import javax.portlet.ActionResponse;
import javax.portlet.PortletConfig;
import javax.portlet.PortletContext;
import javax.portlet.PortletException;
import javax.portlet.PortletMode;
import javax.portlet.PortletModeException;
import javax.portlet.PortletPreferences;
import javax.portlet.PortletRequest;
import javax.portlet.PortletRequestDispatcher;
import javax.portlet.PortletResponse;
import javax.portlet.PortletSession;
import javax.portlet.ReadOnlyException;
import javax.portlet.RenderRequest;
import javax.portlet.RenderResponse;
import javax.portlet.WindowState;
import javax.portlet.WindowStateException;

import org.apache.commons.beanutils.BeanUtils;

import org.apache.portals.bridges.frameworks.model.ModelBean;
import org.apache.portals.bridges.frameworks.model.PortletApplicationModel;
import org.apache.portals.bridges.frameworks.spring.PortletApplicationModelImpl;
import org.apache.portals.bridges.velocity.GenericVelocityPortlet;

/**
 * SpringVelocityPortlet
 * 
 * @author <a href="mailto:taylor@apache.org">David Sean Taylor </a>
 * @version $Id: GenericFrameworkPortlet.java,v 1.1 2004/11/04 18:09:33 taylor
 *          Exp $
 */
public class GenericFrameworkPortlet extends GenericVelocityPortlet
{

    /**
     * Init Parameter: default spring configuration property
     */
    private static final String INITPARAM_SPRING_CONFIG = "spring-configuration";

    /**
     * Init Parameter: default velocity configuration property
     */
    private static final String INITPARAM_VALIDATOR_CONFIG = "validator-configuration";

    private static final String PREFS_SUFFIX = ".prefs";

    private static final String SESSION_ERROR_MESSAGES = "portals.bridges.framework.errors";

    /**
     * Action signature for calling velocity portlet actions
     */
    private static final Class[] VELOCITY_PORTLET_ACTION_SIGNATURE =
    { ActionRequest.class, ActionResponse.class, Object.class};

    private static PortletApplicationModel model = null;

    private static Object semaphore = new Object();

    public GenericFrameworkPortlet()
    {
    }

    public void setExternalSupport(Map map)
    {
        model.setExternalSupport(map);
    }

    public void init(PortletConfig config) throws PortletException
    {
        super.init(config);
        String springConfig = this.getInitParameter(INITPARAM_SPRING_CONFIG);
        if (springConfig == null) { throw new PortletException("Spring Configuration file not specified"); }

        String validatorConfig = this.getInitParameter(INITPARAM_VALIDATOR_CONFIG);

        synchronized (semaphore)
        {
            if (null == model)
            {
                model = new PortletApplicationModelImpl(springConfig, validatorConfig);
                model.init(config);
            }
        }
    }

    /**
     * Invoke the velocity portlet pipeline: (1) determine the logical view (2)
     * restore state from Form to Bean (3) validate the bean -- or -- (2)
     * restore state from Form to Prefs
     * 
     * (4) execute the velocity action (5) forward to another view
     *  
     */
    public void processAction(ActionRequest request, ActionResponse response) throws PortletException, IOException
    {
        // (1) Determine the current view
        String view = determineLogicalView(request);

        Object bean = null;
        ModelBean mb = model.getModelBean(view);

        if (mb.getBeanType() == ModelBean.PREFS_MAP)
        {
            // (2) restore state from Form to Prefs
            bean = formToPrefs(request, view, mb);
        }
        else
        {
            // (2) restore state from Form to Bean
            bean = formToBean(request, view, mb);
        }

        String forward = null;

        // (3) validate the bean
        ResourceBundle bundle = this.getPortletConfig().getResourceBundle(request.getLocale());
        Map errors = model.validate(bean, view, bundle);
        if (errors.isEmpty())
        {
            request.getPortletSession().removeAttribute(SESSION_ERROR_MESSAGES, PortletSession.PORTLET_SCOPE);

            // (4) execute the velocity action
            String action = request.getParameter(FrameworkConstants.BRIDGES_ACTION);
            if (null == action)
            {
                if (mb.getBeanType() == ModelBean.PREFS_MAP)
                {
                    // store prefs
                    storePreferences(request, (Map) bean);
                }

                forward = model.getForward(view, ForwardConstants.SUCCESS);
            }
            else
            {
                // call the specified action in the post params
                String actionForward = invokeVelocityPortletAction(action, request, response, bean);
                forward = model.getForward(actionForward);
            }
        }
        else
        {
            // failed validation
            request.getPortletSession().setAttribute(SESSION_ERROR_MESSAGES, errors, PortletSession.PORTLET_SCOPE);
            forward = model.getForward(view, ForwardConstants.FAILURE);
        }

        clearBeanFromSession(request, mb);
        
        // (5) forward to another view
        forwardToView(request, response, forward);

    }

    protected void forwardToView(ActionRequest request, ActionResponse response, String forward)
    {
        if (forward == null) { return; } // stay on same page

        String logicalView = null;
        PortletMode newMode = null;
        StringTokenizer tokenizer = new StringTokenizer(forward, ForwardConstants.DELIMITER);
        while (tokenizer.hasMoreTokens())
        {
            String token = tokenizer.nextToken();
            if (token.startsWith(ForwardConstants.MODE_PREFIX))
            {
                newMode = setPortletMode(response, token.substring(ForwardConstants.MODE_PREFIX.length()));
            }
            else if (token.startsWith(ForwardConstants.STATE_PREFIX))
            {
                setWindowState(response, token.substring(ForwardConstants.STATE_PREFIX.length()));
            }
            else
            {
                logicalView = token;
            }
        }
        if (logicalView != null)
        {
            setLogicalView(request, response, logicalView, newMode);
        }

    }

    private void setWindowState(ActionResponse response, String forward)
    {
        try
        {
            if (forward.equals(ForwardConstants.MAXIMIZED))
            {
                response.setWindowState(WindowState.MAXIMIZED);
            }
            else if (forward.equals(ForwardConstants.MINIMIZED))
            {
                response.setWindowState(WindowState.MINIMIZED);
            }
            else if (forward.equals(ForwardConstants.NORMAL))
            {
                response.setWindowState(WindowState.NORMAL);
            }
        }
        catch (WindowStateException e)
        {
        }
    }

    private PortletMode setPortletMode(ActionResponse response, String forward)
    {
        PortletMode mode = null;
        try
        {
            if (forward.equals(ForwardConstants.VIEW))
            {
                response.setPortletMode(PortletMode.VIEW);
                mode = PortletMode.VIEW;
            }
            else if (forward.equals(ForwardConstants.EDIT))
            {
                response.setPortletMode(PortletMode.EDIT);
                mode = PortletMode.EDIT;
            }
            else if (forward.equals(ForwardConstants.HELP))
            {
                response.setPortletMode(PortletMode.HELP);
                mode = PortletMode.HELP;
            }
        }
        catch (PortletModeException e)
        {
        }
        return mode;
    }

    protected void storePreferences(PortletRequest request, Map bean) throws IOException, PortletException
    {
        String key = "none";

        try
        {
            PortletPreferences prefs = request.getPreferences();
            Iterator it = bean.entrySet().iterator();
            while (it.hasNext())
            {
                Map.Entry entry = (Map.Entry) it.next();
                key = (String) entry.getKey();
                if (!prefs.isReadOnly(key))
                {
                    prefs.setValue(key, (String) entry.getValue());
                }
            }
            prefs.store();
        }
        catch (ReadOnlyException roe)
        {
            throw new PortletException("Failed to set preference " + key + ", value is readonly");
        }

    }

    /**
     * Get the current logical view based on velocity.view request parameter If
     * the request parameter is not found, fall back to init param
     * 
     * @param request
     * @return the current view
     * @throws PortletException
     */
    protected String determineLogicalView(PortletRequest request) throws PortletException
    {        
        String view = null;
        PortletSession session = request.getPortletSession();
        if (request.getPortletMode().equals(PortletMode.VIEW))
        {
            view  = request.getParameter(FrameworkConstants.VIEW_VIEW_MODE);
            if (view == null)
            {
                view = (String)session.getAttribute(FrameworkConstants.VIEW_VIEW_MODE);
                //view = request.getParameter(FrameworkConstants.VIEW_VIEW_MODE);
                if (view == null)
                {
                    view = this.getDefaultViewPage();
                }
            }
        }
        else if (request.getPortletMode().equals(PortletMode.EDIT))
        {
            view  = request.getParameter(FrameworkConstants.VIEW_EDIT_MODE);
            if (view == null)
            {            
                view = (String)session.getAttribute(FrameworkConstants.VIEW_EDIT_MODE);
                //view = request.getParameter(FrameworkConstants.VIEW_EDIT_MODE);
                if (view == null)
                {
                    view = this.getDefaultEditPage();
                }
            }
        }
        else if (request.getPortletMode().equals(PortletMode.HELP))
        {
            view  = request.getParameter(FrameworkConstants.VIEW_HELP_MODE);
            if (view == null)
            {            
                view = (String)session.getAttribute(FrameworkConstants.VIEW_HELP_MODE);
                //view = request.getParameter(FrameworkConstants.VIEW_HELP_MODE);
                if (view == null)
                {
                    view = this.getDefaultHelpPage();
                }
            }
        }
        if (null == view) 
        { 
            throw new PortletException(
                "Portlet error: cant find view resource for portlet: "
                        + this.getPortletName()); 
        }
        return view;
    }
    
    protected void setLogicalView(PortletRequest request, PortletResponse response, String view, PortletMode newMode)
    {
        PortletSession session = request.getPortletSession();
        if (request.getPortletMode().equals(PortletMode.VIEW))
        {
            session.setAttribute(FrameworkConstants.VIEW_VIEW_MODE, view);                
        }
        else if (request.getPortletMode().equals(PortletMode.EDIT))
        {
            session.setAttribute(FrameworkConstants.VIEW_EDIT_MODE, view);                
        }
        else if (request.getPortletMode().equals(PortletMode.HELP))
        {
            session.setAttribute(FrameworkConstants.VIEW_HELP_MODE, view);                
        }
    }

    protected Object formToBean(ActionRequest request, String view, ModelBean mb) throws PortletException
    {

        // try to get the bean from the session first
        Object bean = getBeanFromSession(request, mb);
        if (bean == null)
        {
            bean = model.createBean(mb);
            if (bean == null) { throw new PortletException("Portlet Action error in creating bean for view: " + view); }
            putBeanInSession(request, mb, bean);
        }

        Map params = request.getParameterMap();
        try
        {
            BeanUtils.populate(bean, params);
        }
        catch (Exception e)
        {
            throw new PortletException("Portlet Action error in  populating bean: " + mb.getBeanName(), e);
        }
        return bean;
    }

    protected Object formToPrefs(ActionRequest request, String view, ModelBean mb) throws PortletException
    {
        Map params = request.getParameterMap();
        Map bean = (Map) request.getPortletSession().getAttribute(view + PREFS_SUFFIX);
        if (bean == null)
        {
            PortletPreferences prefs = request.getPreferences();

            bean = model.createPrefsBean(mb, prefs.getMap());

            request.getPortletSession().setAttribute(view + PREFS_SUFFIX, bean);
        }

        try
        {
            Iterator it = params.entrySet().iterator();
            while (it.hasNext())
            {
                Map.Entry entry = (Map.Entry) it.next();
                Object value = entry.getValue();
                String key = (String) entry.getKey();
                if (null == bean.get(key))
                {
                    continue;
                }
                if (value instanceof String)
                {
                    bean.put(key, value);
                }
                else if (value instanceof String[])
                {
                    bean.put(key, ((String[]) value)[0]);
                }
            }
        }
        catch (Exception e)
        {
            throw new PortletException("Portlet Action error in  populating bean: ", e);
        }
        return bean;
    }

    /**
     * Invokes a specific Velocity Portlet Action All portlet actions must have
     * the signature:
     * 
     * String methodName(ActionRequest request, ActionResponse response)
     * 
     * @param methodName
     */
    protected String invokeVelocityPortletAction(String methodName, ActionRequest request, ActionResponse response,
            Object bean) throws PortletException
    {
        try
        {
            Method method = this.getClass().getMethod(methodName, VELOCITY_PORTLET_ACTION_SIGNATURE);
            Object[] parameters =
            { request, response, bean};
            String result = (String) method.invoke(this, parameters);
            return result;
        }
        catch (Exception e)
        {
            throw new PortletException("Failed to invoke portlet action: " + methodName, e);
        }
    }

    public void doView(RenderRequest request, RenderResponse response) throws PortletException, IOException
    {
        doRender(request, response);
    }

    public void doHelp(RenderRequest request, RenderResponse response) throws PortletException, IOException
    {
        doRender(request, response);
    }

    public void doEdit(RenderRequest request, RenderResponse response) throws PortletException, IOException
    {
        doRender(request, response);
    }

    protected void doRender(RenderRequest request, RenderResponse response) throws PortletException, IOException
    {
        String view = determineLogicalView(request);
        if (view == null) { throw new PortletException("Logical View not found: " + view); }

        String template = model.getTemplate(view);
        if (template == null) { throw new PortletException("Template not found for Logical View: " + view); }

        ModelBean mb = model.getModelBean(view);
        switch (mb.getBeanType())
        {
        case ModelBean.PREFS_MAP:
            preferencesToContext(request, view, mb);
            break;
        case ModelBean.POJO:
            beanToContext(request, view, mb);
            break;
        }
        putRequestVariable(request, FrameworkConstants.FORWARD_TOOL, new Forwarder(model, request, response));
        Map errors = (Map) request.getPortletSession().getAttribute(SESSION_ERROR_MESSAGES,
                PortletSession.PORTLET_SCOPE);
        if (errors != null)
        {
            putRequestVariable(request, "ERRORS", errors);
        }
        request.setAttribute(FrameworkConstants.MODEL_TOOL, model);

        PortletContext context = getPortletContext();
        PortletRequestDispatcher rd = context.getRequestDispatcher(template);
        rd.include(request, response);
    }

    private void beanToContext(RenderRequest request, String view, ModelBean mb)
    {
        Object bean;

        String key = null;
        if (mb.getLookupKey() != null)
        {
            key = (String) request.getAttribute(mb.getLookupKey());
        }
        
        if (key != null)
        {
            bean = model.lookupBean(mb, key);
            if (bean != null)
                putBeanInSession(request, mb, bean);
        }
        else
        {
            bean = getBeanFromSession(request, mb);
        }
        if (bean == null)
        {
            bean = model.createBean(mb);
            if (bean == null) { return; }
            putBeanInSession(request, mb, bean);
        }
        putRequestVariable(request, mb.getBeanName(), bean);
    }

    private void preferencesToContext(RenderRequest request, String view, ModelBean mb)
    {
        Map bean = (Map) request.getPortletSession().getAttribute(view + PREFS_SUFFIX);
        if (bean == null)
        {
            PortletPreferences prefs = request.getPreferences();
            bean = model.createPrefsBean(mb, prefs.getMap());
            putBeanInSession(request, mb, bean);
        }
        putRequestVariable(request, FrameworkConstants.PREFS_VARIABLE, bean);
    }

    private Object getBeanFromSession(PortletRequest request, ModelBean mb)
    {
        return request.getPortletSession().getAttribute(makeModelBeanKey(mb));
    }

    private void clearBeanFromSession(PortletRequest request, ModelBean mb)
    {
        request.getPortletSession().removeAttribute(makeModelBeanKey(mb));
    }
    
    public void startNewRecord(PortletRequest request, String view)
    {
        ModelBean mb = model.getModelBean(view);
        if (mb != null)
            clearBeanFromSession(request, mb);
    }
    
    private void putBeanInSession(PortletRequest request, ModelBean mb, Object bean)
    {
        if (bean instanceof Serializable)
        {
            request.getPortletSession().setAttribute(makeModelBeanKey(mb), bean);
        }
    }

    private String makeModelBeanKey(ModelBean mb)
    {
        return "ModelBean:" + mb.getBeanName();
    }

    /**
     * Specific for Velocity
     * 
     * @param name
     * @param value
     */
    protected void putRequestVariable(RenderRequest request, String name, Object value)
    {
        request.setAttribute(name, value);
    }

}