/********************************************************************
 * Copyright © 2004-2005 VHA. All rights reserved
 ********************************************************************/
package gov.va.med.fw.ui.struts;

// Java classes
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

// Library classes
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.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.web.struts.LookupDispatchActionSupport;

// Framwork classes
import gov.va.med.fw.ui.UIConstants;
import gov.va.med.fw.util.StringUtils;
import gov.va.med.fw.validation.ActionValidation;
import gov.va.med.fw.conversion.ConversionService;

/**
 * <p>
 * An abstract action class that dispatches to a public method for processing a
 * request. This class is marked as abstract to force a derived class to provide
 * its own implementation for processing a http request coming from jsp page. A
 * public method provided in a derived class is looked up in the following
 * order:
 * 
 * <p>
 * <strong>1. </strong>If a derived class overrides a getKeyMethodMap() method
 * to return a map containing method names mapped to a request parameters, a
 * public method whose name is mapped to a request parameter specified in map
 * will be called. This is useful in cases where an HTML form has multiple
 * submit buttons with the same name. The button name is specified by the
 * <code>parameter</code> property of the corresponding ActionMapping. To
 * configure the use of this action in your <code>struts-config.xml</code> file,
 * create an entry like this:
 * </p>
 * 
 * <pre>
 *   &lt;action path="/test"
 *           type="org.example.MyAction"
 *           name="MyForm"
 *          scope="request"
 *          input="/test.jsp"
 *      parameter="action"/&gt;
 * </pre>
 * <p>
 * 
 * which will use the value of the request parameter named "action" to locate
 * the corresponding key in ApplicationResources. For example, you might have
 * the following ApplicationResources.properties:
 * </p>
 * 
 * <pre>
 *    button.add=Add Record
 *    button.delete=Delete Record
 * </pre>
 * <p>
 * 
 * And your JSP would have the following format for submit buttons:
 * </p>
 * 
 * <pre>
 *   &lt;html:form action="/test"&gt;
 *    &lt;html:submit property="action"&gt;
 *      &lt;bean:message key="button.add"/&gt;
 *    &lt;/html:submit&gt;
 *    &lt;html:submit property="action"&gt;
 *      &lt;bean:message key="button.delete"/&gt;
 *    &lt;/html:submit&gt;
 *  &lt;/html:form&gt;
 * </pre>
 * <p>
 * 
 * Your subclass must implement both getKeyMethodMap and the methods defined in
 * the map. An example of such implementations are:
 * </p>
 * 
 * <pre>
 *  protected Map getKeyMethodMap() {
 *      Map map = new HashMap();
 *      map.put("button.add", "add");
 *      map.put("button.delete", "delete");
 *      return map;
 *  }
 * 
 *  public ActionForward add(ActionMapping mapping,
 *          ActionForm form,
 *          HttpServletRequest request,
 *          HttpServletResponse response)
 *          throws IOException, ServletException {
 *      // do add
 *      return mapping.findForward("success");
 *  }
 * 
 *  public ActionForward delete(ActionMapping mapping,
 *          ActionForm form,
 *          HttpServletRequest request,
 *          HttpServletResponse response)
 *          throws IOException, ServletException {
 *      // do delete
 *      return mapping.findForward("success");
 *  }
 *  <p>
 * 
 *  <strong>Notes</strong> - If duplicate values exist for the keys returned by
 *  getKeys, only the first one found will be returned. If no corresponding key
 *  is found then an exception will be thrown. 
 * 
 * <p>
 * <strong>2.</strong> If a request parameter whose name is specified by the 
 * parameter property of the corresponding action mapping is mapped to a public
 * method name, a public method will be called.  This is useful for developers 
 * who prefer to combine many similar actions into a single Action class, in 
 * order to simplify their application design.</p>
 * 
 * <p>To configure the use of this action in your
 * <code>struts-config.xml</code> file, create an entry like this:</p>
 * 
 * <code>
 *   &lt;action path="/saveSubscription"
 *           type="org.apache.struts.actions.DispatchAction"
 *           name="subscriptionForm"
 *          scope="request"
 *          input="/subscription.jsp"
 *      parameter="method"/&gt;
 * </code>
 * 
 * <p>which will use the value of the request parameter named "method"
 * to pick the appropriate "execute" method, which must have the same
 * signature (other than method name) of the standard Action.execute
 * method.  For example, you might have the following three methods in the
 * same action:</p>
 * <ul>
 * <li>public ActionForward delete(ActionMapping mapping, ActionForm form,
 *     HttpServletRequest request, HttpServletResponse response)
 *     throws Exception</li>
 * <li>public ActionForward insert(ActionMapping mapping, ActionForm form,
 *     HttpServletRequest request, HttpServletResponse response)
 *     throws Exception</li>
 * <li>public ActionForward update(ActionMapping mapping, ActionForm form,
 *     HttpServletRequest request, HttpServletResponse response)
 *     throws Exception</li>
 * </ul>
 * <p>and call one of the methods with a URL like this:</p>
 * <code>
 *   http://localhost:8080/myapp/saveSubscription.do?method=update
 * </code>
 * 
 * <p>
 * <strong>3.</strong> If mapping parameter is indeed a public method name, a 
 * method will be called,  This is useful to combine many similar actions into 
 * a singular action class
 * 
 * <p>To configure the use of this action in your
 * <code>struts-config.xml</code> file, create an entry like this:</p>
 * 
 * <code>
 *   &lt;action path="/saveSubscription"
 *           type="org.apache.struts.actions.DispatchAction"
 *           name="subscriptionForm"
 *          scope="request"
 *          input="/subscription.jsp"
 *      parameter="delete"/&gt;
 * </code>
 * 
 * <p>which will use the value of the mapping parameter named "delete"
 * to pick the appropriate public method, which must have the same
 * signature (other than method name) of the standard Action.execute
 * method.  For example, you might have the following three methods in the
 * same action:</p>
 * <ul>
 * <li>public ActionForward delete(ActionMapping mapping, ActionForm form,
 *     HttpServletRequest request, HttpServletResponse response)
 *     throws Exception</li>
 * <li>public ActionForward insert(ActionMapping mapping, ActionForm form,
 *     HttpServletRequest request, HttpServletResponse response)
 *     throws Exception</li>
 * <li>public ActionForward update(ActionMapping mapping, ActionForm form,
 *     HttpServletRequest request, HttpServletResponse response)
 *     throws Exception</li>
 * </ul>
 * 
 * @author  Vu Le
 * @version 1.0
 * 
 * @author Muddaiah Ranga, Andrew Pach //Added hooks for Action(business) validation
 * @version 3.0
 */
public abstract class ServiceBrokerAction extends LookupDispatchActionSupport
		implements ActionValidation {

	// The conversion service
	protected ConversionService conversionService;

	protected transient Log logger = LogFactory.getLog(getClass());

	/**
	 * Performs business validation. This method will be called after
	 * validateForm() method in the action form is called. This method will be
	 * called only when there are no custom validation errors. Also, this method
	 * is called if the validation is enabled in the action mapping. The
	 * subclasses of this class needs to override this method and perform rules
	 * based or other business related validation.
	 * 
	 * @param request
	 *            The servlet request we are processing
	 * @param response
	 *            The servlet response we are creating
	 * @param form
	 *            The ActionForm instance we are populating
	 * @param mapping
	 *            The ActionMapping we are using
	 * 
	 * @return The business validation messages
	 * 
	 * @exception Exception
	 *                if any error occurs
	 */
	public ActionMessages validateAction(ActionMapping mapping,
			ActionForm form, HttpServletRequest request,
			HttpServletResponse response) throws Exception {
		return null;
	}

	/**
	 * After standard declarative validation is performed and one or more errors
	 * found, Struts will automatically forward the request onward. Some actions
	 * many need to populate reference data (like available dropdown menu items)
	 * in order for the page to display correctly. This callback method should
	 * be used to populate this reference data. It is called after errors have
	 * been created for any reason, either via declarative, form-based or
	 * action-based validation. * @param request The servlet request we are
	 * processing
	 * 
	 * @param response
	 *            The servlet response we are creating
	 * @param form
	 *            The ActionForm instance we are populating
	 * @param mapping
	 *            The ActionMapping we are using
	 */
	public void failedValidationCallback(ActionMapping mapping,
			ActionForm form, HttpServletRequest request,
			HttpServletResponse response) {
	}

	/**
	 * Process the specified HTTP request, and create the corresponding HTTP
	 * response (or forward to another web component that will create it).
	 * Return an <code>ActionForward</code> instance describing where and how
	 * control should be forwarded, or <code>null</code> if the response has
	 * already been completed.
	 * 
	 * @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
	 * 
	 * @exception Exception
	 *                if an exception occurs
	 */
	public ActionForward execute(ActionMapping mapping, ActionForm form,
			HttpServletRequest request, HttpServletResponse response)
			throws Exception {
		// Identify the request parameter containing the method name
		String parameter = mapping.getParameter();
		if (parameter == null) {
			String message = messages.getMessage("dispatch.handler",
					mapping.getPath());
			throw new ServletException(message);
		}
		String methodName = this.getMethodName(mapping, form, request,
				response, parameter);
		if (StringUtils.isNotEmpty(methodName)) {
			log.info("Dispatch method name is null or empty");
		}
		return dispatchMethod(mapping, form, request, response, methodName);
	}

	/**
	 * Sets the conversion service that can be used to convert form beans to
	 * VO's and visa-versa. It is encouraged to use this approach rather than an
	 * AbstractVOAdapter since this approach supports some automated conversion.
	 * 
	 * @param conversionService
	 *            The conversion service to set.
	 */
	public void setConversionService(ConversionService conversionService) {
		this.conversionService = conversionService;
	}

	/**
	 * Gets the conversion service.
	 * 
	 * @return The conversion service.
	 */
	public ConversionService getConversionService() {
		return this.conversionService;
	}

	/**
	 * Returns the method name, given a parameter's value. The original method
	 * is modified to combine LookupDispatch, Dispatch and Action features. If
	 * the getKeyMethodMap() is implemented, gets the method name using
	 * getMethodName() method of LookupDispatchAction class. If the
	 * getKeyMethodMap() method is not implemented use the parameter value if
	 * present as the method name, otherwise, use the parameter name as the
	 * method name.
	 * 
	 * @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.
	 */
	protected String getMethodName(ActionMapping mapping, ActionForm form,
			HttpServletRequest request, HttpServletResponse response,
			String parameter) throws Exception {
		String methodName = null;

		// Identify the method name to be dispatched to.
		// dispatchMethod() will call unspecified() if name is null
		if (StringUtils.isNotEmpty(parameter)) {
			if (getKeyMethodMap() != null) {
				log.info("Looking for a method whose name is specified in a map in a getKeyMethodMap method");
				methodName = super.getMethodName(mapping, form, request,
						response, parameter);
			} else {
				// If there is, use a request parameter's value as a method name
				// to
				// look up for a public method in the derived action class to
				// execute.
				methodName = request.getParameter(parameter);
				if (StringUtils.isEmpty(methodName)) {
					log.info("A request parameter was missing. Use a mapping parameter as a method name");

					// If the request parameter is not present, use a mapping
					// method
					// property as a method name to look for a public method
					methodName = parameter;
				}
			}
		}
		return methodName;
	}

	protected Map getKeyMethodMap() {
		return null;
	}

	protected Map getApplicationData(final HttpServletRequest request) {

		// Get the appData out of the session
		HttpSession session = request.getSession();
		Map appData = (Map) session.getAttribute(UIConstants.APP_DATA);
		if (appData == null) {
			appData = new HashMap();
			session.setAttribute(UIConstants.APP_DATA, appData);
		}
		return appData;
	}

	/**
	 * Helper to update the particular sandbox of data
	 * 
	 * @param data
	 *            Data to add to the sandbox
	 */
	protected void updateSandbox(HttpServletRequest request, Map data) {

		Map sandbox = getSandbox(request);
		if (data != null) {
			sandbox.putAll(data);
		} else {
			// If a null is passed, assume that we want to clear all map entries
			// we can change this to not do anything. putAll will throw is null
			// is passed in.
			sandbox.clear();
		}
	}

	/**
	 * Helper to put an entry into the particular SEVIS sandbox.
	 * 
	 * @param request
	 *            The Http Servlet Request
	 * @param key
	 *            The unique key to use for putting and retrieving a value from
	 *            the sandbox.
	 * @param value
	 *            The value to put in the sandbox.
	 */
	protected void putSandboxEntry(HttpServletRequest request, Object key,
			Object value) {
		if (key != null && value != null)
			getSandbox(request).put(key, value);
	}

	/**
	 * Helper to get an entry from the particular sandbox.
	 * 
	 * @param request
	 *            The Http Servlet Request
	 * @param key
	 *            The key in the sandbox which will be used to get a previously
	 *            stored value.
	 */
	protected Object getSandboxEntry(HttpServletRequest request, Object key) {
		return (key != null) ? getSandbox(request).get(key) : null;
	}

	/**
	 * Helper to remove an entry from the particular sandbox.
	 * 
	 * @param request
	 *            The Http Servlet Request
	 * @param key
	 *            The key in the sandbox which will be used to remove a
	 *            previously stored value.
	 */
	protected Object removeSandboxEntry(HttpServletRequest request, Object key) {
		return (key != null) ? getSandbox(request).remove(key) : null;
	}

	/**
	 * Cleans the particular sandbox.
	 */
	protected void cleanSandbox(HttpServletRequest request) {
		getSandbox(request).clear();
	}

	/**
	 * Helper to get the general sandbox. Override in subclass for specific
	 * sandbox implementation. For example, to have a private Map for your
	 * module's usage, override this to get it out of the Map returned from
	 * getApplicationData().
	 */
	protected Map getSandbox(HttpServletRequest request) {
		return getApplicationData(request);
	}

	/**
	 * Helper to get the general sandbox. Override in subclass for specific
	 * sandbox implementation. For example, to set a private Map for your
	 * module's usage, override this to set it in the Map returned from
	 * getApplicationData().
	 * 
	 * @param newSandbox
	 *            The new Map for the Sandbox
	 */
	protected void setSandbox(HttpServletRequest request, Map newSandbox) {
		// since getApplicationData() will never return null, just do an update
		updateSandbox(request, newSandbox);
	}

	public Log getLogger() {
		return logger;
	}
}