org.sakaiproject.cheftool.VelocityPortletPaneledAction.java Source code

Java tutorial

Introduction

Here is the source code for org.sakaiproject.cheftool.VelocityPortletPaneledAction.java

Source

/**********************************************************************************
 * $URL$
 * $Id$
 ***********************************************************************************
 *
 * Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008 The Sakai Foundation
 *
 * Licensed under the Educational Community 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.opensource.org/licenses/ECL-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.sakaiproject.cheftool;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.text.DateFormat;
import java.text.SimpleDateFormat;

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

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.sakaiproject.authz.cover.SecurityService;
import org.sakaiproject.cheftool.api.Alert;
import org.sakaiproject.cheftool.api.Menu;
import org.sakaiproject.cheftool.menu.MenuEntry;
import org.sakaiproject.component.cover.ComponentManager;
import org.sakaiproject.component.cover.ServerConfigurationService;
import org.sakaiproject.content.api.ContentHostingService;
import org.sakaiproject.courier.api.ObservingCourier;
import org.sakaiproject.event.api.SessionState;
import org.sakaiproject.event.api.UsageSession;
import org.sakaiproject.event.cover.UsageSessionService;
import org.sakaiproject.tool.api.Placement;
import org.sakaiproject.tool.api.Session;
import org.sakaiproject.tool.api.Tool;
import org.sakaiproject.tool.api.ToolException;
import org.sakaiproject.tool.api.ToolSession;
import org.sakaiproject.tool.cover.SessionManager;
import org.sakaiproject.tool.cover.ToolManager;
import org.sakaiproject.util.EditorConfiguration;
import org.sakaiproject.util.ParameterParser;
import org.sakaiproject.util.ResourceLoader;
import org.sakaiproject.util.Validator;
import org.sakaiproject.util.Web;
import org.sakaiproject.vm.ActionURL;

/**
 * <p>
 * VelocityPortletPaneledAction ...
 * </p>
 */
@SuppressWarnings("deprecation")
public abstract class VelocityPortletPaneledAction extends ToolServlet {

    private static final long serialVersionUID = 1L;

    /** Our logger. */
    private static Log M_log = LogFactory.getLog(VelocityPortletPaneledAction.class);

    /** message bundle */
    private static ResourceLoader rb = new ResourceLoader("velocity-tool");

    protected static final String BUTTON = "eventSubmit_";

    /** The currently active helper mode static class. */
    public static final String STATE_HELPER = "vppa.helper";

    protected static final String STATE_MODE = "mode";

    protected static final String STATE_OBSERVER = "obsever";

    protected static final String STATE_ACTION = "action";

    protected static final String STATE_NEW_PANEL = "state:new_panel";

    /** The name of the context variable containing the identifier for the site's root content collection */
    protected static final String CONTEXT_SITE_COLLECTION_ID = "vppa_site_collection_id";

    /** The name of the context variable containing the access URL for the site's root content collection */
    protected static final String CONTEXT_SITE_COLLECTION_URL = "vppa_site_collection_url";

    /** The panel name of the main panel - append the tool's id. */
    protected static final String LAYOUT_MAIN = "Main";

    /** The name of the param used for CSRF protection */
    protected static final String SAKAI_CSRF_TOKEN = "sakai_csrf_token";

    /** Constants to handle helper situations */
    protected static final String HELPER_LINK_MODE = "link_mode";
    protected static final String HELPER_MODE_DONE = "helper.done";

    private ContentHostingService contentHostingService;

    public VelocityPortletPaneledAction() {
        contentHostingService = (ContentHostingService) ComponentManager.get(ContentHostingService.class.getName());
    }

    protected class MyLogger {
        public void warn(String channel, String msg) {
            M_log.warn(msg);
        }

        public void warn(String channel, String msg, Throwable e) {
            M_log.warn(msg, e);
        }

        public void debug(String channel, String msg) {
            M_log.debug(msg);
        }

        public void debug(String channel, String msg, Throwable e) {
            M_log.debug(msg, e);
        }

        public void info(String channel, String msg) {
            M_log.info(msg);
        }

        public void info(String channel, String msg, Throwable e) {
            M_log.info(msg, e);
        }

        public void error(String channel, String msg) {
            M_log.error(msg);
        }

        public void error(String channel, String msg, Throwable e) {
            M_log.error(msg, e);
        }

        // to support: if (Log.getLogger("chef").isDebugEnabled())
        public MyLogger getLogger(String name) {
            return this;
        }

        public boolean isDebugEnabled() {
            return M_log.isDebugEnabled();
        }

        public boolean isWarnEnabled() {
            return M_log.isWarnEnabled();
        }

        public boolean isInfoEnabled() {
            return M_log.isInfoEnabled();
        }
    }

    protected MyLogger Log = new MyLogger();

    protected void initState(SessionState state, VelocityPortlet portlet, JetspeedRunData rundata) {
        HttpServletRequest req = rundata.getRequest();
        Session session = SessionManager.getCurrentSession();

        if (getVmReference("is_wireless_device", req) == null) {
            Object c = session.getAttribute("is_wireless_device");
            setVmReference("is_wireless_device", c, req);
        }

        // Set a CSRF token for velocity-based forms
        if (getVmReference(SAKAI_CSRF_TOKEN, req) == null) {
            Object csrfToken = session.getAttribute(UsageSessionService.SAKAI_CSRF_SESSION_ATTRIBUTE);
            setVmReference(SAKAI_CSRF_TOKEN, csrfToken, req);
        }
    }

    /**
     * Compute the deliver address for the current request. Compute the client window id, based on the float state
     * 
     * @param state
     *        The tool state.
     * @param toolId
     *        The tool instance id, which might be used as part of the client window id if floating.
     * @return The client window id, based on the float state.
     */
    protected String clientWindowId(SessionState state, String toolId) {
        // TODO: drop the params

        // get the Sakai session
        Session session = SessionManager.getCurrentSession();

        // get the current tool placement
        Placement placement = ToolManager.getCurrentPlacement();

        // compute our courier delivery address: this placement in this session
        String deliveryId = session.getId() + placement.getId();

        return deliveryId;

    } // clientWindowId

    /**
     * Compute the courier update html element id for the main panel - add "." and other names for inner panels.
     * 
     * @param toolId
     *        The tool (portlet) id.
     * @return The courier update html element id for the main panel.
     */
    public static String mainPanelUpdateId(String toolId) {
        // TODO: who should be responsible for "Main" here? It's a Portal thing... -ggolden
        return Validator.escapeJavascript("Main" + toolId);

    } // mainPanelUpdateId

    /**
     * Compute the courier update html element id for the title panel.
     * 
     * @param toolId
     *        The tool (portlet) id.
     * @return The courier update html element id for the title panel.
     */
    public static String titlePanelUpdateId(String toolId) {
        // TODO: who should be responsible for "Title" here? It's a Portal thing... -ggolden
        return Validator.escapeJavascript("Title" + toolId);

    } // titlePanelUpdateId

    /**
     * Add another string to the alert message.
     * 
     * @param state
     *        The session state.
     * @param message
     *        The string to add.
     */
    public static void addAlert(SessionState state, String message) {
        String soFar = (String) state.getAttribute(STATE_MESSAGE);
        if (soFar != null) {
            soFar = soFar + "\n\n" + message;
        } else {
            soFar = message;
        }
        state.setAttribute(STATE_MESSAGE, soFar);

    } // addAlert

    /**
     * Switch to a new panel
     * 
     * @param state
     *        The session state.
     * @param newPanel
     *        The new panel name
     */
    public static void switchPanel(SessionState state, String newPanel) {
        state.setAttribute(STATE_NEW_PANEL, newPanel);

    } // addAlert

    /**
     * Initialize for the first time the session state for this session. If overridden in a sub-class, make sure to call super.
     * 
     * @param state
     *        The session state.
     * @param req
     *        The current portlet request.
     * @param res
     *        The current portlet response.
     */
    protected void initState(SessionState state, HttpServletRequest req, HttpServletResponse res) {
        super.initState(state, req, res);

        // call the old initState:
        VelocityPortlet portlet = (VelocityPortlet) req.getAttribute(ATTR_PORTLET);
        JetspeedRunData rundata = (JetspeedRunData) req.getAttribute(ATTR_RUNDATA);

        initState(state, portlet, rundata);

    } // initState

    /**
     * Update for this request processing the session state. If overridden in a sub-class, make sure to call super.
     * 
     * @param state
     *        The session state.
     * @param req
     *        The current portlet request.
     * @param res
     *        The current portlet response.
     */
    protected void updateState(SessionState state, HttpServletRequest req, HttpServletResponse res) {
        super.updateState(state, req, res);

        // the old way has just initState, so...
        VelocityPortlet portlet = (VelocityPortlet) req.getAttribute(ATTR_PORTLET);
        JetspeedRunData rundata = (JetspeedRunData) req.getAttribute(ATTR_RUNDATA);

        initState(state, portlet, rundata);

    } // updateState

    /**
     * Dispatch to a "do" method based on reflection. Override ToolServlet to support the old "build" ways.
     * 
     * @param methodBase
     *        The base name of the method to call.
     * @param methodExt
     *        The end name of the method to call.
     * @param req
     *        The HttpServletRequest.
     * @param res
     *        The HttpServletResponse
     */
    protected void toolModeDispatch(String methodBase, String methodExt, HttpServletRequest req,
            HttpServletResponse res) throws ToolException {
        // the context wraps our real vm attribute set
        Context context = (Context) req.getAttribute(ATTR_CONTEXT);

        // other wrappers
        VelocityPortlet portlet = (VelocityPortlet) req.getAttribute(ATTR_PORTLET);
        JetspeedRunData rundata = (JetspeedRunData) req.getAttribute(ATTR_RUNDATA);

        // "panel" is used to identify the specific panel in the URL
        context.put("param_panel", ActionURL.PARAM_PANEL);

        // set the "action"
        context.put("action", getState(req).getAttribute(STATE_ACTION));

        // set the "pid"
        context.put("param_pid", ActionURL.PARAM_PID);
        context.put("pid", getPid(req));

        String collectionId = contentHostingService
                .getSiteCollection(ToolManager.getCurrentPlacement().getContext());
        context.put(CONTEXT_SITE_COLLECTION_ID, collectionId);

        // indicate which WYSIWYG editor to use in legacy tools
        String editor = EditorConfiguration.getWysiwigEditor();

        context.put("sakai_editor", editor);
        context.put("editorConfig", new EditorConfiguration());

        UsageSession session = UsageSessionService.getSession();
        if (session != null) {
            // SAK-23047 Set the proper country code in the chef_start generated markup
            String userId = session.getUserId();
            ResourceLoader rl = new ResourceLoader(userId);
            Locale locale = rl.getLocale();
            String languageCode = locale.getLanguage();
            String countryCode = locale.getCountry();
            if (countryCode != null && countryCode.length() > 0) {
                languageCode += "_" + countryCode;
            }
            context.put("language", languageCode);
            context.put("dir", rl.getOrientation(locale));

            String browserId = session.getBrowserId();
            if (UsageSession.WIN_IE.equals(browserId) || UsageSession.WIN_MZ.equals(browserId)
                    || UsageSession.WIN_NN.equals(browserId) || UsageSession.MAC_MZ.equals(browserId)
                    || UsageSession.MAC_NN.equals(browserId)) {
                context.put("wysiwyg", "true");
            }
        }

        try {
            // dispatch panels (Note: panel must be in the URL, not the body - this is not from the parsed params)
            String panel = ((ParameterParser) req.getAttribute(ATTR_PARAMS)).getString(ActionURL.PARAM_PANEL);

            /*
             * TODO: float support from before... // special case for floating and the Main panel: if (LAYOUT_MAIN.equals(panel)) { if (handleFloat(portlet, context, rundata, state)) return; }
             */
            if (panel == null || "".equals(panel) || "null".equals(panel)) {
                // default to main panel
                panel = LAYOUT_MAIN;
            } else {
                // sanitize value
                panel = panel.replaceAll("[\r\n]", "");
            }

            context.put("panel", panel);

            // form a method name "build" + panel name (first letter caps) + "PanelContext"
            // buildPanelContext( VelocityPortlet, Context, ControllerState, RunData )
            Class[] types = new Class[4];
            types[0] = VelocityPortlet.class;
            types[1] = Context.class;
            types[2] = RunData.class;
            types[3] = SessionState.class;

            // let our extension classes override the pannel name for the method
            String methodName = panelMethodName(panel);

            Method method = getClass().getMethod(methodName, types);

            Object[] args = new Object[4];
            args[0] = portlet;
            args[1] = context;
            args[2] = rundata;
            args[3] = getState(req);
            String template = (String) method.invoke(this, args);

            // if the method did something like a redirect, we don't want to try to put out any more
            if (!res.isCommitted()) {
                if (template == null) {
                    // pick the template for the panel - the base + "-" + panel
                    template = (String) getContext(rundata).get("template") + "-" + panel;
                }

                // the vm file needs a path and an extension
                template = "/vm/" + template + ".vm";

                // setup for old style alert
                StringBuilder buf = new StringBuilder();
                String msg = (String) getState(req).getAttribute(STATE_MESSAGE);
                if (msg != null) {
                    buf.append(msg);
                    getState(req).removeAttribute(STATE_MESSAGE);
                }
                Alert alert = getAlert(req);
                if (!alert.isEmpty()) {
                    buf.append(alert.peekAlert());
                    setVmReference(ALERT_ATTR, alert, req);
                }
                if (buf.length() > 0) {
                    setVmReference("alertMessage", buf.toString(), req);
                }

                // setup for old style validator
                setVmReference("validator", m_validator, req);

                // set standard no-cache headers
                setNoCacheHeaders(res);

                // add a standard header
                includeVm("chef_header.vm", req, res);

                includeVm(template, req, res);

                // add a standard footer
                includeVm("chef_footer.vm", req, res);
            }
        } catch (NoSuchMethodException e) {
            try {
                res.sendError(HttpServletResponse.SC_BAD_REQUEST, "NoSuchMethodException for panel name");
            } catch (IOException e1) {
                // ignore
            }
        } catch (IllegalAccessException e) {
            throw new ToolException(e);
        } catch (InvocationTargetException e) {
            throw new ToolException(e);
        } catch (ServletException e) {
            throw new ToolException(e);
        }

    } // toolModeDispatch

    /**
     * Allow extension classes to control which build method gets called for this pannel
     * @param panel
     * @return
     */
    protected String panelMethodName(String panel) {
        return "build" + panel + "PanelContext";
    }

    /**
     * Process a Portlet action.
     */
    public void processAction(HttpServletRequest req, HttpServletResponse res) {
        // lets use the parsed params
        JetspeedRunData rundata = (JetspeedRunData) req.getAttribute(ATTR_RUNDATA);
        ParameterParser params = rundata.getParameters();

        // see if there's an action parameter, whose value has the action to use
        String action = params.get(PARAM_ACTION);

        // if that's not present, see if there's a combination name with the action encoded in the name
        if (action == null) {
            Iterator<String> names = params.getNames();
            while (names.hasNext()) {
                String name = names.next();
                if (name.startsWith(BUTTON)) {
                    action = name.substring(BUTTON.length());
                    break;
                }
            }
        } else {
            // SAK-18148 look for first non empty action
            if ("".equals(action)) {
                String[] actions = params.getStrings(PARAM_ACTION);
                if (actions != null) {
                    for (int i = 0; i < actions.length; i++) {
                        if (!"".equals(actions[i])) {
                            action = actions[i];
                            break;
                        }
                    }
                }
            }
        }

        // process the action if present
        if (action != null) {
            if (!checkCSRFToken(req, rundata, action))
                return;

            // if we have an active helper, send the action there
            String helperClass = (String) getState(req).getAttribute(STATE_HELPER);
            if (helperClass != null) {
                helperActionDispatch("", action, req, res, helperClass);
            } else {
                actionDispatch("", action, req, res);
            }

            // Handle shortcut return from a tool helper between its post and redirect
            // Helper does this in an Action method:
            // SessionManager.getCurrentToolSession().setAttribute(HELPER_LINK_MODE, HELPER_MODE_DONE);
            // and then returns

            ToolSession toolSession = SessionManager.getCurrentToolSession();
            if (HELPER_MODE_DONE.equals(toolSession.getAttribute(HELPER_LINK_MODE))) {
                Tool tool = ToolManager.getCurrentTool();

                String url = (String) toolSession.getAttribute(tool.getId() + Tool.HELPER_DONE_URL);
                toolSession.removeAttribute(tool.getId() + Tool.HELPER_DONE_URL);
                toolSession.removeAttribute(HELPER_LINK_MODE);

                if (url != null) {
                    try {
                        res.sendRedirect(url);
                        return;
                    } catch (IOException e) {
                        M_log.warn("IOException: ", e);
                    }
                }
            }

            // Continue non-shortcut processing
            // redirect to the tool's registration's url, with the tool id (pid) and panel
            // Tool tool = (Tool) req.getAttribute(ATTR_TOOL);
            // TODO: redirect url? pannel? placement id?
            String url = Web.returnUrl(req, null);

            String panel = ((ParameterParser) req.getAttribute(ATTR_PARAMS)).getString(ActionURL.PARAM_PANEL);
            String newPanel = (String) getState(req).getAttribute(STATE_NEW_PANEL);
            getState(req).removeAttribute(STATE_NEW_PANEL);

            if (newPanel != null)
                panel = newPanel;

            if (panel == null || panel.equals("") || panel.equals("null")) {
                panel = MAIN_PANEL;
            } else {
                // sanitize value
                panel = panel.replaceAll("[\r\n]", "");
            }
            String redirect = url + "?" + ActionURL.PARAM_PANEL + "=" + panel;

            try {
                res.sendRedirect(redirect);
            } catch (IOException e) {
            }
        } else {
            M_log.debug("processAction: no action");
        }

    } // processAction

    public boolean checkCSRFToken(HttpServletRequest request, RunData rundata, String action) {
        ParameterParser params = rundata.getParameters();

        // if user if manipulating data via POST, check for presence of CSRF token
        if ("POST".equals(rundata.getRequest().getMethod())) {
            // check if tool id is in list of tools to skip the CSRF check
            Placement placement = ToolManager.getCurrentPlacement();
            String toolId = null;
            if (placement != null) {
                toolId = placement.getToolId();
            }

            boolean skipCSRFCheck = false;
            String[] insecureTools = ServerConfigurationService.getStrings("velocity.csrf.insecure.tools");
            if (toolId != null && insecureTools != null) {
                for (int i = 0; i < insecureTools.length; i++) {
                    if (StringUtils.equalsIgnoreCase(toolId, insecureTools[i])) {
                        if (M_log.isDebugEnabled()) {
                            M_log.debug("Will skip all CSRF checks on toolId=" + toolId);
                        }
                        skipCSRFCheck = true;
                        break;
                    }
                }
            }

            // if the user is not logged in, then do not worry about csrf
            Session session = SessionManager.getCurrentSession();
            boolean loggedIn = session.getUserId() != null;

            if (loggedIn && !skipCSRFCheck) {
                // If the attribute is missing, it is likely an internal error,
                // not an error in the tool
                Object sessionAttr = SessionManager.getCurrentSession()
                        .getAttribute(UsageSessionService.SAKAI_CSRF_SESSION_ATTRIBUTE);
                if (sessionAttr == null) {
                    M_log.warn("Missing CSRF Token session attribute: " + action + "; toolId=" + toolId);
                    return false;
                }

                String csrfToken = params.getString(SAKAI_CSRF_TOKEN);
                String sessionToken = sessionAttr.toString();
                if (csrfToken == null || sessionToken == null || !StringUtils.equals(csrfToken, sessionToken)) {
                    M_log.warn("CSRF Token mismatched or missing on velocity action: " + action + "; toolId="
                            + toolId);
                    return false;
                }
                if (M_log.isDebugEnabled()) {
                    M_log.debug(
                            "CSRF token (" + csrfToken + ") matches on action: " + action + "; toolId=" + toolId);
                }
            }
        }
        return true;
    }

    /**
     * Dispatch to a "processAction" method based on reflection.
     * 
     * @param methodBase
     *        The base name of the method to call.
     * @param methodExt
     *        The end name of the method to call.
     * @param req
     *        The HttpServletRequest.
     * @param res
     *        The HttpServletResponse
     * @throws PortletExcption,
     *         IOException, just like the "do" methods.
     */
    protected void actionDispatch(String methodBase, String methodExt, HttpServletRequest req,
            HttpServletResponse res) {
        String methodName = null;
        try {
            // the method signature
            Class[] signature = new Class[2];
            signature[0] = RunData.class;
            signature[1] = Context.class;

            // the method name
            methodName = methodBase + methodExt;

            // find a method of this class with this name and signature
            Method method = getClass().getMethod(methodName, signature);

            // parameters - the context for a "do" should not be used, so we will send in null
            Object[] args = new Object[2];
            args[0] = (JetspeedRunData) req.getAttribute(ATTR_RUNDATA);
            args[1] = null;

            // make the call
            method.invoke(this, args);
        } catch (NoSuchMethodException e) {
            // try for a single parameter call
            try {
                // the method signature
                Class[] signature = new Class[1];
                signature[0] = RunData.class;

                // the method name
                methodName = methodBase + methodExt;

                // find a method of this class with this name and signature
                Method method = getClass().getMethod(methodName, signature);

                // parameters - the context for a "do" should not be used, so we will send in null
                Object[] args = new Object[1];
                args[0] = (JetspeedRunData) req.getAttribute(ATTR_RUNDATA);

                // make the call
                method.invoke(this, args);
            } catch (NoSuchMethodException e2) {
                M_log.warn("Exception calling method " + methodName + " " + e2);
            } catch (IllegalAccessException e2) {
                M_log.warn("Exception calling method " + methodName + " " + e2);
            } catch (InvocationTargetException e2) {
                String xtra = "";
                if (e2.getCause() != null)
                    xtra = " (Caused by " + e2.getCause() + ")";
                M_log.warn("Exception calling method " + methodName + " " + e2 + xtra, e2);

            }
        } catch (IllegalAccessException e) {
            M_log.warn("Exception calling method " + methodName + " " + e);
        } catch (InvocationTargetException e) {
            String xtra = "";
            if (e.getCause() != null)
                xtra = " (Caused by " + e.getCause() + ")";
            M_log.warn("Exception calling method " + methodName + " " + e + xtra, e);
        }

    } // actionDispatch

    /**
     * Dispatch to a "processAction" method based on reflection in a helper class.
     * 
     * @param methodBase
     *        The base name of the method to call.
     * @param methodExt
     *        The end name of the method to call.
     * @param req
     *        The HttpServletRequest.
     * @param res
     *        The HttpServletResponse
     * @throws PortletExcption,
     *         IOException, just like the "do" methods.
     */
    protected void helperActionDispatch(String methodBase, String methodExt, HttpServletRequest req,
            HttpServletResponse res, String className) {
        String methodName = null;
        try {
            // the method signature
            Class[] signature = new Class[1];
            signature[0] = RunData.class;

            // the method name
            methodName = methodBase + methodExt;

            Class cls = Class.forName(className);

            // find a method of this class with this name and signature
            Method method = cls.getMethod(methodName, signature);

            // parameters - the context for a "do" should not be used, so we will send in null
            Object[] args = new Object[1];
            args[0] = (JetspeedRunData) req.getAttribute(ATTR_RUNDATA);

            // make the call
            method.invoke(this, args);
        } catch (ClassNotFoundException e) {
            M_log.warn("Exception helper class not found " + e);
        } catch (NoSuchMethodException e) {
            M_log.warn("Exception calling method " + methodName + " " + e);
        } catch (IllegalAccessException e) {
            M_log.warn("Exception calling method " + methodName + " " + e);
        } catch (InvocationTargetException e) {
            String xtra = "";
            if (e.getCause() != null)
                xtra = " (Caused by " + e.getCause() + ")";
            M_log.warn("Exception calling method " + methodName + " " + e + xtra);
        }

    } // helperActionDispatch

    /**
     * This is used to get "template" from the map, the default template registered for the tool in chef_tools.xreg.
     */
    protected Map<String, String> getContext(RunData data) {
        // get template from the servlet config
        String template = getServletConfig().getInitParameter("template");
        Map<String, String> rv = new HashMap<String, String>();
        rv.put("template", template);

        return rv;
    }

    // OPTIONS SUPPORT
    public static final String STATE_OBSERVER2 = "obsever2";

    public static final String STATE_PRESENCE_OBSERVER = "presence_observer";

    public static final String STATE_FLOAT = "float";

    public static final String STATE_TOOL = "tool";

    public static final String STATE_MESSAGE = "message";

    /** Standard modes. */
    public static final String MODE_OPTIONS = "options";

    /**
     * Handle a request to set options.
     */
    public void doOptions(RunData runData, Context context) {
        // ignore if not allowed
        if (!allowedToOptions()) {
            return;
            //msg = "you do not have permission to set options for this Worksite.";
        }

        Placement placement = ToolManager.getCurrentPlacement();
        String pid = null;
        if (placement != null)
            pid = placement.getId();
        SessionState state = ((JetspeedRunData) runData).getPortletSessionState(pid);

        // go into options mode
        state.setAttribute(STATE_MODE, MODE_OPTIONS);

        // disable auto-updates while editing
        disableObservers(state);

        // if we're not in the main panel for this tool, schedule an update of the main panel
        String currentPanelId = runData.getParameters().getString(ActionURL.PARAM_PANEL);
        if (!LAYOUT_MAIN.equals(currentPanelId)) {
            String mainPanelId = mainPanelUpdateId(pid);
            schedulePeerFrameRefresh(mainPanelId);
        }

    } // doOptions

    /**
     * Complete the options process with a save.
     */
    protected void saveOptions() {
        // ask the current placement to save
        Placement placement = ToolManager.getCurrentPlacement();
        if (placement != null) {
            placement.save();
        }

    } // saveOptions

    /**
     * Cancel the options process.
     */
    protected void cancelOptions() {
        // TODO: how to indicate that we need to get clean placement options into the current tool?

    } // cancelOptions

    /**
     * Add the options to the menu bar, if allowed.
     * 
     * @param bar
     *        The menu bar to add to,
     * @param ref
     *        The resource reference to base the security decision upon.
     */
    protected void addOptionsMenu(Menu bar, JetspeedRunData data) // %%% don't need data -ggolden
    {
        if (allowedToOptions()) {
            bar.add(new MenuEntry(rb.getString("options"), "doOptions"));
        }

    } // addOptionsMenu

    /**
     * Check if the current user is allowed to do options for the current context (site based)
     * @return true if the user is allowed to modify the current context's options, false if not.
     */
    protected boolean allowedToOptions() {
        Placement placement = ToolManager.getCurrentPlacement();
        String context = null;
        if (placement != null)
            context = placement.getContext();

        // TODO: stolen from site -ggolden
        if (SecurityService.unlock("site.upd", "/site/" + context)) {
            return true;
        }

        return false;
    }

    /**
     * Disable any observers registered in state in STATE_OBSERVER or STATE_OBSERVER2
     * 
     * @param state
     *        The session state.
     */
    public static void disableObservers(SessionState state) {
        ObservingCourier observer = (ObservingCourier) state.getAttribute(STATE_OBSERVER);
        if (observer != null) {
            observer.disable();
        }

        observer = (ObservingCourier) state.getAttribute(STATE_OBSERVER2);
        if (observer != null) {
            observer.disable();
        }

    } // disableObservers

    /**
     * Enable any observers registered in state in STATE_OBSERVER or STATE_OBSERVER2
     * 
     * @param state
     *        The session state.
     */
    public static void enableObservers(SessionState state) {
        ObservingCourier observer = (ObservingCourier) state.getAttribute(STATE_OBSERVER);
        if (observer != null) {
            observer.enable();
        }

        observer = (ObservingCourier) state.getAttribute(STATE_OBSERVER2);
        if (observer != null) {
            observer.enable();
        }

    } // enableObservers

    /**
     * Tell the main observer we have just delivered.
     * 
     * @param state
     *        The session state.
     */
    public static void justDelivered(SessionState state) {
        ObservingCourier observer = (ObservingCourier) state.getAttribute(STATE_OBSERVER);
        if (observer != null) {
            observer.justDelivered();
        }
    }

    /**
     * Handle the "reset tool" option from the Title bar.
     */
    public void doReset(RunData runData, Context context) {
        // access the portlet element id to find "our" state (i.e. the state for this portlet)
        String peid = ((JetspeedRunData) runData).getJs_peid();
        SessionState state = ((JetspeedRunData) runData).getPortletSessionState(peid);

        // clear this state
        resetTool(state);

        // // make sure the Main panel is updated
        String main = VelocityPortletPaneledAction.mainPanelUpdateId(peid);
        schedulePeerFrameRefresh(main);

    } // doReset

    /**
     * Reset the tool (state) to "home" conditions. Default here is to clear everything from state.
     * 
     * @param state
     *        The tool's session state.
     */
    protected void resetTool(SessionState state) {
        state.clear();

    } // resetTool

    /**
     * Add some standard references to the vm context.
     * 
     * @param request
     *        The render request.
     * @param response
     *        The render response.
     */
    @SuppressWarnings("unchecked")
    protected void setVmStdRef(HttpServletRequest request, HttpServletResponse response) {
        // pick up all the super ones
        super.setVmStdRef(request, response);

        // add a "$config" which accesses the config service
        setVmReference("config", ServerConfigurationService.getInstance(), request);

        // add the pid
        setVmReference("pid", getPid(request), request);

        // check for a scheduled peer frame or focus refresh
        ToolSession session = SessionManager.getCurrentToolSession();
        if (session != null) {
            if (session.getAttribute(ATTR_TOP_REFRESH) != null) {
                setVmReference("topRefresh", Boolean.TRUE, request);
                session.removeAttribute(ATTR_TOP_REFRESH);
            }

            Set<String> ids = (Set<String>) session.getAttribute(ATTR_FRAME_REFRESH);
            if (ids != null) {
                setVmReference("frameRefresh", ids, request);
                session.removeAttribute(ATTR_FRAME_REFRESH);
            }

            String focusPath = (String) session.getAttribute(ATTR_FRAME_FOCUS);
            if (focusPath != null) {
                setVmReference("focusChange", focusPath, request);
                session.removeAttribute(ATTR_FRAME_FOCUS);
            }
        }
        Tool tool = ToolManager.getCurrentTool();
        if (tool != null) {
            setVmReference("toolTitle", tool.getTitle(), request);
        }
    }

    /**
     * Setup the vm context for a courier
     * 
     * @param request
     */
    protected void setVmCourier(HttpServletRequest request, int refresh) {
        // the url for the chat courier
        Placement placement = ToolManager.getCurrentPlacement();
        if (placement != null) {
            String userId = SessionManager.getCurrentSessionUserId();
            StringBuilder url = new StringBuilder(Web.serverUrl(request));
            url.append("/courier/");
            url.append(placement.getId());
            url.append("?userId=");
            url.append(userId);

            setVmReference("courier", url.toString(), request);
            setVmReference("courierTimeout", Integer.toString(refresh), request);
        }
    }

    /** A Context bound into the request attributes. */

    protected final static String ATTR_CONTEXT = "sakai.wrapper.context";

    /** A PortletConfig bound into the request attributes. */
    protected final static String ATTR_CONFIG = "sakai.wrapper.config";

    /** A VelocityPortlet bound into the request attributes. */
    protected final static String ATTR_PORTLET = "sakai.wrapper.portlet";

    /** A JetspeedRunData bound into the request attributes. */
    protected final static String ATTR_RUNDATA = "sakai.wrapper.rundata";

    /**
     * Respond to a request by dispatching to a portlet like "do" method based on the portlet mode and tool mode
     */
    protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException {
        // set in VmServlet
        ParameterParser params = (ParameterParser) req.getAttribute(ATTR_PARAMS);

        // we will need some covers... Note: parameters are parsed (i.e. files are read) here
        Context context = new Context(this, req);
        Placement placement = ToolManager.getCurrentPlacement();
        PortletConfig config = new PortletConfig(getServletConfig(), placement.getPlacementConfig(),
                placement.getTool().getRegisteredConfig(), placement);
        VelocityPortlet portlet = new VelocityPortlet(getPid(req), config);
        JetspeedRunData rundata = new JetspeedRunData(req, getState(req), getPid(req), params);

        req.setAttribute(ATTR_CONTEXT, context);
        req.setAttribute(ATTR_CONFIG, config);
        req.setAttribute(ATTR_PORTLET, portlet);
        req.setAttribute(ATTR_RUNDATA, rundata);

        super.doGet(req, res);
    }

    // Set up RunData if it's not already set up    

    protected void checkRunData(HttpServletRequest req) {
        if (req.getAttribute(ATTR_RUNDATA) != null)
            return;

        // set in VmServlet
        ParameterParser params = (ParameterParser) req.getAttribute(ATTR_PARAMS);
        JetspeedRunData rundata = new JetspeedRunData(req, getState(req), getPid(req), params);
        req.setAttribute(ATTR_RUNDATA, rundata);
    }

    /** Tool session attribute name used to schedule a peer frame refresh. */
    public static final String ATTR_FRAME_REFRESH = "sakai.vppa.frame.refresh";

    /** Tool session attribute name used to schedule a whole page refresh. */
    public static final String ATTR_TOP_REFRESH = "sakai.vppa.top.refresh";

    /** Tool session attribute name used to schedule a focus change. */
    public static final String ATTR_FRAME_FOCUS = "sakai.vppa.frame.focus";

    /**
     * Schedule a refresh for whole page
     */
    protected void scheduleTopRefresh() {
        ToolSession session = SessionManager.getCurrentToolSession();

        // add to (or create) our set of ids to refresh
        if (session.getAttribute(ATTR_TOP_REFRESH) == null) {
            session.setAttribute(ATTR_TOP_REFRESH, Boolean.TRUE);
        }
    }

    /**
     * Schedule a refresh for a peer frame.
     * 
     * @param id
     *        The peer frame's id.
     */
    @SuppressWarnings("unchecked")
    protected void schedulePeerFrameRefresh(String id) {
        ToolSession session = SessionManager.getCurrentToolSession();

        // add to (or create) our set of ids to refresh
        Set<String> soFar = (Set<String>) session.getAttribute(ATTR_FRAME_REFRESH);
        if (soFar == null) {
            soFar = new HashSet<String>();
            session.setAttribute(ATTR_FRAME_REFRESH, soFar);
        }
        soFar.add(id);
    }

    /**
     * Schedule a focus change.
     * 
     * @param path
     *        The desired focus path elements
     */
    protected void scheduleFocusRefresh(String[] path) {
        ToolSession session = SessionManager.getCurrentToolSession();

        // make the js string from the elements
        String jsArray = "[";
        for (int i = 0; i < path.length; i++) {
            if (i > 0) {
                jsArray = jsArray + ",";
            }
            jsArray = jsArray + " \"" + path[i] + "\"";
        }
        jsArray = jsArray + " ]";

        // save it for the next display
        session.setAttribute(ATTR_FRAME_FOCUS, jsArray);
    }

    /**
     ** Return a String array containing the "m", "d", "y" characters (corresponding to month, day, year) 
     ** in the locale specific order
     **/
    private static String[] DEFAULT_FORMAT_ARRAY = new String[] { "m", "d", "y" };

    public String[] getDateFormatString() {
        SimpleDateFormat sdf = (SimpleDateFormat) DateFormat.getDateInstance(DateFormat.SHORT, rb.getLocale());
        String[] formatArray = sdf.toPattern().split("[/\\-\\.]");
        for (int i = 0; i < formatArray.length; i++)
            formatArray[i] = formatArray[i].trim().substring(0, 1).toLowerCase();

        if (formatArray.length != DEFAULT_FORMAT_ARRAY.length) {
            M_log.warn("Unknown date format string (using default): " + sdf.toPattern());
            return DEFAULT_FORMAT_ARRAY;
        } else {
            return formatArray;
        }
    }

    private static final String[] DEFAULT_TIME_FORMAT_ARRAY = new String[] { "h", "m", "a" };

    /**
     ** Return a String array containing the "h", "m", "a", or "H" characters (corresponding to hour, minute, am/pm, or 24-hour)
     ** in the locale specific order
     **/
    public String[] getTimeFormatString() {
        SimpleDateFormat sdf = (SimpleDateFormat) DateFormat.getTimeInstance(DateFormat.SHORT, rb.getLocale());
        String format = sdf.toPattern();

        Set<String> formatSet = new LinkedHashSet<String>();
        char curChar;
        char lastChar = 0;
        for (int i = 0; i < format.length(); i++) {
            curChar = format.charAt(i);
            if ((curChar == 'h' || curChar == 'm' || curChar == 'a' || curChar == 'H') && curChar != lastChar) {
                formatSet.add(String.valueOf(curChar));
                lastChar = curChar;
            }
        }

        String[] formatArray = formatSet.toArray(new String[formatSet.size()]);
        if (formatArray.length != DEFAULT_TIME_FORMAT_ARRAY.length
                && formatArray.length != DEFAULT_TIME_FORMAT_ARRAY.length - 1) {
            M_log.warn("Unknown time format string (using default): " + format);
            return DEFAULT_TIME_FORMAT_ARRAY.clone();
        } else {
            return formatArray;
        }
    }
} // class VelocityPortletPaneledAction