package org.appfuse.webapp.action;


import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.commons.beanutils.ConvertUtils;
import org.apache.commons.beanutils.converters.LongConverter;
import org.apache.commons.beanutils.converters.StringConverter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.struts.Globals;
import org.apache.struts.action.ActionErrors;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
import org.apache.struts.action.ActionMessages;
import org.apache.struts.actions.LookupDispatchAction;
import org.apache.struts.config.MessageResourcesConfig;
import org.apache.struts.config.ModuleConfig;
import org.apache.struts.util.MessageResources;
import org.appfuse.Constants;
import org.appfuse.model.Resume;
import org.appfuse.model.User;
import org.appfuse.service.ResumeManager;
import org.appfuse.util.ConvertUtil;
import org.appfuse.util.CurrencyConverter;
import org.appfuse.util.DateConverter;
import org.appfuse.webapp.util.SslUtil;
import org.springframework.context.ApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;


/**
 * Implementation of <strong>Action</strong> that contains base methods for
 * logging and conducting pre/post perform actions. This class is intended to
 * be a base class for all Struts actions.
 *
 * <p>
 * <a href="BaseAction.java.html"><i>View Source</i></a>
 * </p>
 *
 * @author <a href="mailto:[email protected]">Matt Raible</a>
 * @version $Revision: 1.9 $ $Date: 2004/03/31 13:04:24 $
 */
public class BaseAction extends LookupDispatchAction {

    protected static Log log = LogFactory.getLog(BaseAction.class);
    public static final String SECURE = "secure";
    private static Long defaultLong = null;
    protected ApplicationContext ctx = null;

    static {
        ConvertUtils.register(new CurrencyConverter(), Double.class);
        ConvertUtils.register(new DateConverter(), Date.class);
        ConvertUtils.register(new LongConverter(defaultLong), Long.TYPE);
        ConvertUtils.register(new LongConverter(defaultLong), Long.class);
        ConvertUtils.register(new StringConverter(), String.class);

        if (log.isDebugEnabled()) {
            log.debug("Converters registered...");
        }
    }

    /**
     * Convenience method to bind objects in Actions
     * @param name
     * @return
     */
    public Object getBean(String name) {
        if (ctx == null) {
            ctx = WebApplicationContextUtils
            .getRequiredWebApplicationContext(servlet.getServletContext());
        }
        return ctx.getBean(name);
    }

    /**
     * Convenience method to lookup the user's resumeId
     * @param request
     * @return
     * @throws Exception
     */
    public String getResumeId(HttpServletRequest request) throws Exception {
        ResumeManager mgr = (ResumeManager) getBean("resumeManager");
        User user = getUser(request.getSession());
        if (log.isDebugEnabled()) {
            log.debug("looking up resume for user: '" + user.getUsername() + "'");
        }
        List resumes = mgr.getResumesForUser(user.getId().toString());
        if (resumes.size() > 0) {
            Resume resume = (Resume) resumes.get(0);
            return String.valueOf(resume.getId());
        } else {
            Resume resume = new Resume();
            resume.setUserId(user.getId());
            resume= (Resume) mgr.saveResume(resume);
            return resume.getId().toString();
        }
    }
    
    /**
     * Provides the mapping from resource key to method name
     *
     * @return Resource key / method name map
     */
    public Map getKeyMethodMap() {
        Map map = new HashMap();

        ResourceBundle methods =
            ResourceBundle.getBundle("org.appfuse.webapp.action.LookupMethods");

        Enumeration keys = methods.getKeys();

        while (keys.hasMoreElements()) {
            String key = (String) keys.nextElement();
            map.put(key, methods.getString(key));
        }

        return map;
    }

    /**
     * @see org.appfuse.util.ConvertUtil#convertDates(java.lang.Object, java.lang.Object)
     */
    protected Object convertDates(Object obj, Object form) {
        return ConvertUtil.convertDates(obj, form);
    }

    /**
     * @see org.appfuse.util.ConvertUtil#convert(java.lang.Object)
     */
    protected Object convert(Object o) throws Exception {
        return ConvertUtil.convert(o);
    }
    
    /**
     * @see org.appfuse.util.ConvertUtil#convertLists(java.lang.Object)
     */
    protected Object convertLists(Object o) throws Exception {
        return ConvertUtil.convertLists(o);
    }

    /**
     * Convenience method to initialize messages in a subclass.
     * @param request the current request
     * @return the populated (or empty) messages
     */
    public ActionMessages getMessages(HttpServletRequest request) {
        ActionMessages messages = null;
        HttpSession session = request.getSession(false);

        if (request.getAttribute(Globals.MESSAGE_KEY) != null) {
            messages =
                (ActionMessages) request.getAttribute(Globals.MESSAGE_KEY);
            saveMessages(request, messages);
        } else if (session.getAttribute(Globals.MESSAGE_KEY) != null) {
            messages =
                (ActionMessages) session.getAttribute(Globals.MESSAGE_KEY);
            saveMessages(request, messages);
            session.removeAttribute(Globals.MESSAGE_KEY);
        } else {
            messages = new ActionMessages();
        }

        return messages;
    }

    /**
     * Convenience method to initialize errors in a subclass.
     * @param request the current request
     * @return the populated (or empty) messages
     */
    public ActionErrors getErrors(HttpServletRequest request) {
        ActionErrors errors = null;
        HttpSession session = request.getSession(false);

        if (request.getAttribute(Globals.ERROR_KEY) != null) {
            errors =
                (ActionErrors) request.getAttribute(Globals.ERROR_KEY);
            saveErrors(request, errors);
        } else if (session.getAttribute(Globals.ERROR_KEY) != null) {
            errors =
                (ActionErrors) session.getAttribute(Globals.ERROR_KEY);
            saveErrors(request, errors);
            session.removeAttribute(Globals.ERROR_KEY);
        } else {
            errors = new ActionErrors();
        }

        return errors;
    }
    
    /**
     * Override the execute method in LookupDispatchAction to parse
     * URLs and forward to methods without parameters.  Also will
     * forward to unspecified method when no parameter is present.
     *
     * This is based on the following system:
     *
     * <ul>
     *   <li>edit*.do -> edit method</li>
     *   <li>save*.do -> save method</li>
     *   <li>view*.do -> search method</li>
     * </ul>
     *
     * @param mapping The ActionMapping used to select this instance
     * @param request The HTTP request we are processing
     * @param response The HTTP response we are creating
     * @param form The optional ActionForm bean for this request (if any)
     * @return Describes where and how control should be forwarded.
     * @exception Exception if an error occurs
     */
    public ActionForward execute(ActionMapping mapping, ActionForm form,
                                 HttpServletRequest request,
                                 HttpServletResponse response)
    throws Exception {
        if (isCancelled(request)) {
            ActionForward af = cancelled(mapping, form, request, response);

            if (af != null) {
                return af;
            }
        }

        MessageResources resources = getResources(request);
        // this call grabs any messages in the session and stuffs them in the
        // request
        ActionMessages messages = getMessages(request);

        // Identify the localized message for the cancel button
        String edit = resources.getMessage("button.edit").toLowerCase();
        String save = resources.getMessage("button.save").toLowerCase();
        String search = resources.getMessage("button.search").toLowerCase();
        // view is same as search
        String view = resources.getMessage("button.view").toLowerCase();
        String[] rules = { edit, save, search, view };

        // Identify the request parameter containing the method name
        String parameter = mapping.getParameter();

        String keyName = request.getParameter(parameter);

        if ((keyName == null) || (keyName.length() == 0)) {
            for (int i = 0; i < rules.length; i++) {
                // apply the rules for automatically appending the method name
                if (request.getServletPath().indexOf(rules[i]) > -1) {
                    return dispatchMethod(mapping, form, request, response,
                                          rules[i]);
                }
            }

            return this.unspecified(mapping, form, request, response);
        }

        // Identify the string to lookup
        String methodName =
            getMethodName(mapping, form, request, response, parameter);

        return dispatchMethod(mapping, form, request, response, methodName);
    }

    /**
     * Convenience method for getting an action form base on it's mapped scope.
     *
     * @param mapping The ActionMapping used to select this instance
     * @param request The HTTP request we are processing
     *
     * @return ActionForm the form from the specifies scope, or null if nothing
     *         found
     */
    protected ActionForm getActionForm(ActionMapping mapping,
                                       HttpServletRequest request) {
        ActionForm actionForm = null;

        // Remove the obsolete form bean
        if (mapping.getAttribute() != null) {
            if ("request".equals(mapping.getScope())) {
                actionForm =
                    (ActionForm) request.getAttribute(mapping.getAttribute());
            } else {
                HttpSession session = request.getSession();

                actionForm =
                    (ActionForm) session.getAttribute(mapping.getAttribute());
            }
        }

        return actionForm;
    }

    /**
     * Lookup the method name corresponding to the client request's locale.
     *
     * @param request The HTTP request we are processing
     * @param keyName The parameter name to use as the properties key
     * @param mapping The ActionMapping used to select this instance
     *
     * @return The method's localized name.
     * @throws ServletException if keyName cannot be resolved
     * @since Struts 1.2.0
     */
    protected String getLookupMapName(HttpServletRequest request,
                                      String keyName, ActionMapping mapping)
    throws ServletException {
        // Based on this request's Locale get the lookupMap
        Map lookupMap = null;

        synchronized (localeMap) {
            Locale userLocale = this.getLocale(request);
            lookupMap = (Map) this.localeMap.get(userLocale);

            if (lookupMap == null) {
                lookupMap = this.initLookupMap(request, userLocale);
                this.localeMap.put(userLocale, lookupMap);
            }
        }

        // Find the key for the resource
        String key = (String) lookupMap.get(keyName);

        if (key == null) {
            String message =
                messages.getMessage("dispatch.resource", mapping.getPath(),
                                    keyName);
            throw new ServletException(message);
        }

        // Find the method name
        String methodName = (String) keyMethodMap.get(key);

        if (methodName == null) {
            String message =
                messages.getMessage("dispatch.lookup", mapping.getPath(), key);
            throw new ServletException(message);
        }


        return methodName;
    }

    /**
     * Returns the method name, given a parameter's value.
     *
     * @param mapping The ActionMapping used to select this instance
     * @param form The optional ActionForm bean for this request (if any)
     * @param request The HTTP request we are processing
     * @param response The HTTP response we are creating
     * @param parameter The <code>ActionMapping</code> parameter's name
     *
     * @return The method's name.
     * @since Struts 1.2.0
     */
    protected String getMethodName(ActionMapping mapping, ActionForm form,
                                   HttpServletRequest request,
                                   HttpServletResponse response,
                                   String parameter) throws Exception {
        // Identify the method name to be dispatched to.
        // dispatchMethod() will call unspecified() if name is null
        String keyName = request.getParameter(parameter);

        if ((keyName == null) || (keyName.length() == 0)) {
            return null;
        }

        String methodName = getLookupMapName(request, keyName, mapping);

        return methodName;
    }

    /**
     * Convenience method to get the userForm from the session
     *
     * @param session the current user's session
     * @return the user's populated form from the session
     */
    protected User getUser(HttpSession session) {
        // get the user form from the session
        return (User) session.getAttribute(Constants.USER_KEY);
    }

    /**
     * Method which is dispatched to when the request is a cancel button submit.
     * Subclasses of <code>DispatchAction</code> should override this method if
     * they wish to provide default behavior different than returning null.
     * @since Struts 1.2.0
     */
    protected ActionForward cancelled(ActionMapping mapping, ActionForm form,
                                      HttpServletRequest request,
                                      HttpServletResponse response)
    throws Exception {
        return null;
    }

    /**
     * Method to check and see if https is required for this resource
     *
     * @param mapping The ActionMapping used to select this instance
     * @param request The HTTP request we are processing
     * @param response The HTTP response we are creating
     *
     * @return boolean true if redirection to SSL is needed
     */
    protected boolean checkSsl(ActionMapping mapping,
                               HttpServletRequest request,
                               HttpServletResponse response) {
        String redirectString =
            SslUtil.getRedirectString(request,
                                      getServlet().getServletContext(),
                                      SECURE.equals(mapping.getParameter()));

        if (redirectString != null) {
            log.debug("protocol switch needed, redirecting...");

            try {
                // Redirect the page to the desired URL
                response.sendRedirect(response.encodeRedirectURL(redirectString));

                return true;
            } catch (Exception ioe) {
                log.error("redirect to new protocol failed...");

                // Handle appropriately
            }
        }

        return false;
    }

    /**
     * Convenience method for removing the obsolete form bean.
     *
     * @param mapping The ActionMapping used to select this instance
     * @param request The HTTP request we are processing
     */
    protected void removeFormBean(ActionMapping mapping,
                                  HttpServletRequest request) {
        // Remove the obsolete form bean
        if (mapping.getAttribute() != null) {
            if ("request".equals(mapping.getScope())) {
                request.removeAttribute(mapping.getAttribute());
            } else {
                HttpSession session = request.getSession();

                session.removeAttribute(mapping.getAttribute());
            }
        }
    }

    /**
     * Convenience method to update a formBean in it's scope
     *
     * @param mapping The ActionMapping used to select this instance
     * @param request The HTTP request we are processing
     * @param form The ActionForm
     */
    protected void updateFormBean(ActionMapping mapping,
                                  HttpServletRequest request, Object form) {
        // Remove the obsolete form bean
        if (mapping.getAttribute() != null) {
            if ("request".equals(mapping.getScope())) {
                request.setAttribute(mapping.getAttribute(), form);
            } else {
                HttpSession session = request.getSession();

                session.setAttribute(mapping.getAttribute(), form);
            }
        }
    }

    /**
     * This is the first time this Locale is used so build the reverse lookup Map.
     * Search for message keys in all configured MessageResources for
     * the current module.
     */
    private Map initLookupMap(HttpServletRequest request, Locale userLocale) {
        Map lookupMap = new HashMap();
        this.keyMethodMap = this.getKeyMethodMap();

        ModuleConfig moduleConfig =
            (ModuleConfig) request.getAttribute(Globals.MODULE_KEY);

        MessageResourcesConfig[] mrc =
            moduleConfig.findMessageResourcesConfigs();

        // Look through all module's MessageResources
        for (int i = 0; i < mrc.length; i++) {
            MessageResources resources =
                this.getResources(request, mrc[i].getKey());

            // Look for key in MessageResources
            Iterator iter = this.keyMethodMap.keySet().iterator();

            while (iter.hasNext()) {
                String key = (String) iter.next();
                String text = resources.getMessage(userLocale, key);

                // Found key and haven't added to Map yet, so add the text
                if ((text != null) && !lookupMap.containsKey(text)) {
                    lookupMap.put(text, key);
                }
            }
        }

        return lookupMap;
    }
}