org.dihedron.webmvc.ActionContext.java Source code

Java tutorial

Introduction

Here is the source code for org.dihedron.webmvc.ActionContext.java

Source

/*
 * Copyright (c) 2012-2015, Andrea Funto'. All rights reserved. See LICENSE for details.
 */

package org.dihedron.webmvc;

import static org.dihedron.webmvc.Constants.MILLISECONDS_PER_SECOND;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.security.Principal;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.dihedron.core.properties.Properties;
import org.dihedron.core.regex.Regex;
import org.dihedron.core.strings.Strings;
import org.dihedron.webmvc.exceptions.WebMVCException;
import org.dihedron.webmvc.interceptors.Interceptor;
import org.dihedron.webmvc.protocol.Conversation;
import org.dihedron.webmvc.protocol.HttpMethod;
import org.dihedron.webmvc.protocol.Scope;
import org.dihedron.webmvc.upload.FileUploadConfiguration;
import org.dihedron.webmvc.upload.UploadedFile;
import org.dihedron.webmvc.webserver.WebServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This object provides mediated access to the underlying JSR-286 features, such
 * as session, parameters, remote user information and the like. This interface
 * is a superset of all available functionalities; other classes may provide a
 * restricted view base on the current phase, to help developers discover bugs
 * at compile time instead of having to catch exceptions at run time.
 * 
 * @author Andrea Funto'
 */
public class ActionContext {

    /**
     * The logger.
     */
    private static final Logger logger = LoggerFactory.getLogger(ActionContext.class);

    //   /**
    //    * The default encoding for file names in multipart/form-data requests.
    //    */
    //   private static final String DEFAULT_ENCODING = "UTF-8";

    /**
     * The key under which conversation-scoped attributes are stored in the session.
     */
    protected static final String CONVERSATION_SCOPED_ATTRIBUTES_KEY = "org.dihedron.webmvc.conversation_scoped_attributes";

    /**
     * The key under which sticky-scoped attributes are stored in the application area.
     */
    protected static final String STICKY_SCOPED_ATTRIBUTES_KEY = "org.dihedron.webmvc.sticky_scoped_attributes";

    /**
     * The key under which interceptor data is stored in the session.
     */
    protected static final String INTERCEPTOR_DATA_KEY = "org.dihedron.webmvc.interceptor_data";

    /**
     * The per-thread instance.
     */
    private static ThreadLocal<ActionContext> context = new ThreadLocal<ActionContext>() {
        @Override
        protected ActionContext initialValue() {
            //         logger.trace("creating action context instance for thread {}", Thread.currentThread().getId());
            return new ActionContext();
        }
    };

    /**
     * A reference to the filter configuration, for access to filter-specific
     * information.
     */
    private FilterConfig filter;

    /**
     * The servlet request object.
     */
    private HttpServletRequest request;

    /**
     * The servlet response object.
     */
    private HttpServletResponse response;

    /**
     * The actions' configuration; this map is read only and it's loaded at
     * startup if the URL of a properties file is provided in the web.xml, among
     * the WebMVC controller's initialisation parameters.
     */
    private Properties configuration;

    /**
     * A reference to the {@code WebServer} plug-in; this is useful if the
     * actions need need to exploit web server specific APIs.
     */
    private WebServer server = null;

    //   /**
    //    * The encoding of uploaded file names.
    //    */
    //   private String encoding = DEFAULT_ENCODING;

    /**
     * A map containing names and information about all the form fields in a 
     * multipart/form-data request; if the form is not multipart, the form values 
     * can be retrieved directly from the request in the ordinary way (see 
     * {@link HttpServletRequest#getParameter(String)} for details); if the 
     * request is multipart, then the standard says that parameters will all be 
     * mixed up in the request, and the context will extract them and place them 
     * in this map. Thus, when this map is null, the request is not multipart 
     * and values will be picked from the request, when not null, all parameters
     * (be they form fields or uploaded files) will be available inside this map.
     */
    private Map<String, FileItem> parts = null;

    /**
     * Retrieves the per-thread instance.
     * 
     * @return the per-thread instance.
     */
    private static ActionContext getContext() {
        return context.get();
    }

    /**
     * Binds the thread-local object to the current invocation, by setting
     * references to the various objects (web server plugin, request and
     * response objects etc.) that will be made available to the business method
     * through this context.
     * 
     * @param request
     *   the servlet request object.
     * @param response
     *   the servlet response object.
     * @param configuration
     *   the configuration object, holding properties that may have been loaded at
     *   startup if the proper initialisation parameter is specified in the
     *   web.xml.
     * @param server
     *   a reference to the web server specific plugin.
     * @throws WebMVCException 
     */
    static void bindContext(FilterConfig filter, HttpServletRequest request, HttpServletResponse response,
            Properties configuration, WebServer server, FileUploadConfiguration uploadInfo) throws WebMVCException {
        //      logger.trace("initialising the action context for thread {}", Thread.currentThread().getId());
        getContext().filter = filter;
        getContext().request = request;
        getContext().response = response;
        getContext().configuration = configuration;
        getContext().server = server;

        // this is where we try to retrieve all files (if there are any that were 
        // uploaded) and store them as temporary files on disk; these objects will
        // be accessible as ordinary values under the "FORM" scope through a 
        // custom "filename-to-file" map, which will be clened up when the context
        // is unbound
        //      String encoding = request.getCharacterEncoding();
        //        getContext().encoding = Strings.isValid(encoding)? encoding : DEFAULT_ENCODING;
        //        logger.trace("request encoding is: '{}'", getContext().encoding);

        try {

            // check that we have a file upload request
            if (ServletFileUpload.isMultipartContent(request)) {

                getContext().parts = new HashMap<String, FileItem>();

                logger.trace("handling multipart/form-data request");

                // create a factory for disk-based file items
                DiskFileItemFactory factory = new DiskFileItemFactory();

                // register a tracker to perform automatic file cleanup
                //              FileCleaningTracker tracker = FileCleanerCleanup.getFileCleaningTracker(getContext().filter.getServletContext());
                //              factory.setFileCleaningTracker(tracker);

                // configure the repository (to ensure a secure temporary location 
                // is used and the size of the )
                factory.setRepository(uploadInfo.getRepository());
                factory.setSizeThreshold(uploadInfo.getInMemorySizeThreshold());

                // create a new file upload handler
                ServletFileUpload upload = new ServletFileUpload(factory);
                upload.setSizeMax(uploadInfo.getMaxUploadableTotalSize());
                upload.setFileSizeMax(uploadInfo.getMaxUploadableFileSize());

                // parse the request & process the uploaded items
                List<FileItem> items = upload.parseRequest(request);
                logger.trace("{} items in the multipart/form-data request", items.size());
                for (FileItem item : items) {
                    logger.trace("storing field '{}' (type: '{}') into parts map", item.getFieldName(),
                            item.isFormField() ? "field" : "file");
                    getContext().parts.put(item.getFieldName(), item);
                }
                //           } else {
                //              logger.trace("handling plain form request");
            }
        } catch (FileUploadException e) {
            logger.warn("error handling uploaded file", e);
            throw new WebMVCException("Error handling uploaded file", e);
        }
    }

    /**
     * Cleans up the internal status of the {@code ActionContext} in order to
     * avoid memory leaks due to persisting objects stored in the per-thread
     * local storage; afterwards it removes the thread local entry altogether,
     * so the application server does not complain about left-over data in TLS
     * when re-deploying the application (see Tomcat memory leak detection
     * feature).
     */
    static void unbindContext() {
        logger.trace("removing action context for thread {}", Thread.currentThread().getId());
        getContext().filter = null;
        getContext().request = null;
        getContext().response = null;
        getContext().configuration = null;
        getContext().server = null;
        // remove all files if this is a multipart/form-data request, because
        // the file tracker does not seem to work as expected
        if (isMultiPartRequest()) {
            for (Entry<String, FileItem> entry : getContext().parts.entrySet()) {
                if (!entry.getValue().isFormField() && !entry.getValue().isInMemory()) {
                    File file = ((DiskFileItem) entry.getValue()).getStoreLocation();
                    try {
                        logger.trace("removing uploaded file '{}' from disk...", file.getAbsolutePath());
                        Files.delete(file.toPath());
                        logger.trace("... file deleted");
                    } catch (IOException e) {
                        logger.trace("... error deleting file", e);
                    }
                }
            }
        }
        getContext().parts = null;

        //      if(getContext().parts != null) {
        //         for(Entry<String, FileItem> entry : getContext().parts.entrySet()) {
        //            FileItem part = entry.getValue();
        //            if(!part.isFormField() && !part.isInMemory()) {
        //               logger.trace("releasing on-disk uploaded file '{}'", part.getFieldName());
        //               
        //            }
        //         }
        //      }
        context.remove();
    }

    /**
     * Returns a reference to the servlet context.
     * 
     * @return
     *   a reference to the servlet context.
     */
    public static ServletContext getServletContext() {
        // TODO: need to check on this: getContext().request.getSession().getServletContext();
        return getContext().filter.getServletContext();
    }

    /**
     * Returns a reference to the current application-server-specific plug-in,
     * if available. If no plug-in was loaded, returns null.
     * 
     * @return 
     *   a reference to the current application-server-specific plug-in.
     */
    public static WebServer getApplicationServer() {
        return getContext().server;
    }

    /**
     * Returns the name of the current WebMVC controller instance.
     * 
     * @return 
     *   the current portlet's name.
     */
    public static String getFilterName() {
        return getContext().filter.getFilterName();
    }

    /**
     * Returns the value of the current WebMVC controller filter's
     * initialisation parameter.
     * 
     * @param name
     *   the name of the parameter.
     * @return 
     *   the value of the initialisation parameter for the current WebMVC
     *   controller instance.
     */
    public static String getFilterInitialisationParameter(String name) {
        return getContext().filter.getInitParameter(name);
    }

    /**
     * Returns a string representing the authentication type.
     * 
     * @return 
     *   a string representing the authentication type.
     */
    public static String getAuthType() {
        return getContext().request.getAuthType();
    }

    /**
     * Checks whether the client request came through a secured connection.
     * 
     * @return 
     *   whether the client request came through a secured connection.
     */
    public static boolean isSecure() {
        return getContext().request.isSecure();
    }

    /**
     * Returns the name of the remote authenticated user.
     * 
     * @return 
     *   the name of the remote authenticated user.
     */
    public static String getRemoteUser() {
        return getContext().request.getRemoteUser();
    }

    /**
     * Returns the user principal associated with the request.
     * 
     * @return 
     *   the user principal.
     */
    public static Principal getUserPrincipal() {
        return getContext().request.getUserPrincipal();
    }

    /**
     * Checks whether the user has the given role.
     * 
     * @param role
     *   the name of the role
     * @return 
     *   whether the user has the given role.
     */
    public static boolean isUserInRole(String role) {
        return getContext().request.isUserInRole(role);
    }

    /**
     * Returns the locale associated with the user's request.
     * 
     * @return 
     *   the request locale.
     */
    public static Locale getLocale() {
        return getContext().request.getLocale();
    }

    /**
     * Returns an Enumeration of Locale objects indicating, in decreasing order
     * starting with the preferred locale in which the portal will accept
     * content for this request. The Locales may be based on the Accept-Language
     * header of the client.
     * 
     * @return 
     *   an Enumeration of Locales, in decreasing order, in which the
     *   portal will accept content for this request
     */
    public static Enumeration<Locale> getLocales() {
        return (Enumeration<Locale>) getContext().request.getLocales();
    }

    /**
     * Returns the name of the current web server.
     * 
     * @return 
     *   the name of the current web server.
     */
    public static String getServerName() {
        return getContext().request.getServerName();
    }

    /**
     * Returns the port of the current web server.
     * 
     * @return 
     *   the port of the current web server.
     */
    public static int getServerPort() {
        return getContext().request.getServerPort();
    }

    // /**
    // * Returns the current portlet mode.
    // *
    // * @return
    // * the current portlet mode.
    // */
    // public static PortletMode getPortletMode() {
    // return
    // PortletMode.fromString(getContext().request.getPortletMode().toString());
    // }
    //
    // /**
    // * Sets the current portlet mode; it is preferable not to use this method
    // * directly and let the framework set the portlet mode instead, by
    // * specifying it in the action's results settings.
    // *
    // * @param mode
    // * the new portlet mode.
    // * @throws PortletModeException
    // * if the new portlet mode is not supported by the current portal server
    // * runtime environment.
    // * @throws InvalidPhaseException
    // * if the operation is attempted while in the render phase.
    // */
    // @Deprecated
    // public static void setPortletMode(PortletMode mode) throws
    // PortletModeException, InvalidPhaseException {
    // if(isActionPhase() || isEventPhase()) {
    // if(getContext().request.isPortletModeAllowed(mode)) {
    // logger.trace("changing portlet mode to '{}'", mode);
    // ((StateAwareResponse)getContext().response).setPortletMode(mode);
    // } else {
    // logger.warn("unsupported portlet mode '{}'", mode);
    // }
    // } else {
    // logger.error("trying to change portlet mode in the render phase");
    // throw new
    // InvalidPhaseException("Portlet mode cannot be changed in the render phase.");
    // }
    // }
    //
    // /**
    // * Returns the current portlet window state.
    // *
    // * @return
    // * the current portlet window state.
    // */
    // public static WindowState getWindowState() {
    // return
    // WindowState.fromString(getContext().request.getWindowState().toString());
    // }
    //
    // /**
    // * Sets the current window state; it is preferable not to use this method
    // * directly and let the framework set the portlet window state instead, by
    // * specifying it in the action's results settings.
    // *
    // * @param state
    // * the new window state.
    // * @throws WindowStateException
    // * if the new window state is not supported by the current portal server
    // * runtime environment.
    // * @throws InvalidPhaseException
    // * if the operation is attempted in the render phase.
    // */
    // @Deprecated
    // public static void setWindowState(WindowState state) throws
    // WindowStateException, InvalidPhaseException {
    // if(isActionPhase() || isEventPhase()) {
    // if(getContext().request.isWindowStateAllowed(state)) {
    // logger.trace("changing window state to '{}'", state);
    // ((StateAwareResponse)getContext().response).setWindowState(state);
    // } else {
    // logger.warn("unsupported window state '{}'", state);
    // }
    // } else {
    // logger.error("trying to change window state in the render phase");
    // throw new
    // InvalidPhaseException("Windows state cannot be changed in the render phase.");
    // }
    // }
    //
    // /**
    // * Returns the portlet window ID. The portlet window ID is unique for this
    // * portlet window and is constant for the lifetime of the portlet window.
    // * This ID is the same that is used by the portlet container for scoping
    // * the portlet-scope session attributes.
    // *
    // * @return
    // * the portlet window ID.
    // */
    // public static String getPortletWindowId() {
    // if(getContext().request != null) {
    // return getContext().request.getWindowID();
    // }
    // return null;
    // }
    //
    /**
     * Returns the session ID indicated in the client request. This session ID
     * may not be a valid one, it may be an old one that has expired or has been
     * invalidated. If the client request did not specify a session ID, this
     * method returns null.
     * 
     * @return 
     *   a String specifying the session ID, or null if the request did
     *   not specify a session ID.
     * @see isRequestedSessionIdValid()
     */
    public static String getRequestedSessionId() {
        return getContext().request.getRequestedSessionId();
    }

    /**
     * Checks whether the requested session ID is still valid.
     * 
     * @return 
     *   true if this request has an id for a valid session in the current
     *   session context; false otherwise.
     */
    public static boolean isRequestedSessionIdValid() {
        return getContext().request.isRequestedSessionIdValid();
    }

    /**
     * Returns whether the session id was presented by the client as a cookie.
     * 
     * @return 
     *   whether the session id was presented by the client as a cookie.
     */
    public static boolean isRequestedSessionIdFromCookie() {
        return getContext().request.isRequestedSessionIdFromCookie();
    }

    /**
     * Returns whether the session id was presented by the client as a parameter
     * in the request URL.
     * 
     * @return 
     *   whether the session id was presented by the client as a parameter
     *   in the request URL.
     */
    public static boolean isRequestedSessionIdFromURL() {
        return getContext().request.isRequestedSessionIdFromURL();
    }

    /**
     * Returns whether the session is still valid.
     * 
     * @return 
     *   whether the session is still valid.
     */
    public static boolean isSessionValid() {
        HttpSession session = getContext().request.getSession();
        long elapsed = System.currentTimeMillis() - session.getLastAccessedTime();
        return (elapsed < session.getMaxInactiveInterval() * MILLISECONDS_PER_SECOND);
    }

    /**
     * Returns the number of seconds left before the session gets invalidated by
     * the container.
     * 
     * @return 
     *   the number of seconds left before the session gets invalidated by the 
     *   container.
     */
    public static long getSecondsToSessionInvalid() {
        HttpSession session = getContext().request.getSession();
        long elapsed = System.currentTimeMillis() - session.getLastAccessedTime();
        return (long) ((elapsed - session.getMaxInactiveInterval() * MILLISECONDS_PER_SECOND)
                / MILLISECONDS_PER_SECOND);
    }

    /**
     * Returns the number of seconds since the last access to the session
     * object.
     * 
     * @return 
     *   the number of seconds since the last access to the session object.
     */
    public static long getTimeOfLastAccessToSession() {
        return getContext().request.getSession().getLastAccessedTime();
    }

    /**
     * Returns the maximum amount of inactivity seconds before the session is
     * considered stale.
     * 
     * @return 
     *   the maximum number of seconds before the session is considered stale.
     */
    public static int getMaxInactiveSessionInterval() {
        return getContext().request.getSession().getMaxInactiveInterval();
    }

    /**
     * Sets the session timeout duration in seconds.
     * 
     * @param time
     *   the session timeout duration, in seconds.
     */
    public static void setMaxInactiveSessionInterval(int time) {
        getContext().request.getSession().setMaxInactiveInterval(time);
    }

    /**
     * Returns the part of this request's URL from the protocol name 
     * up to the query string in the first line of the HTTP request.
     * 
     * @return
     *   the request URI.
     */
    public static String getRequestURI() {
        return getContext().request.getRequestURI();
    }

    /**
     * Reconstructs the URL the client used to make the request.
     * 
     * @return
     *   the URL the client used to make the request.
     */
    public static StringBuffer getRequestURL() {
        return getContext().request.getRequestURL();
    }

    /**
     *  Returns any extra path information associated with the URL the 
     *  client sent when it made this request.
     * 
     * @return
     *   any extra path information associated with the URL the
     *   client sent when it made this request.
     */
    public static String getPathInfo() {
        return getContext().request.getPathInfo();
    }

    /**
     *  Returns any extra path information after the servlet name but 
     *  before the query string, and translates it to a real path.
     * 
     * @return
     *   any extra path information after the servlet name but 
     *   before the query string, and translates it to a real path.
     */
    public static String getPathTranslated() {
        return getContext().request.getPathTranslated();
    }

    /**
     * Returns an array containing all of the Cookie properties. This method
     * returns null if no cookies exist.
     * 
     * @return 
     *   the array of cookie properties, or null if no cookies exist.
     */
    public static Cookie[] getCookies() {
        return getContext().request.getCookies();
    }

    /**
     * Adds a cookie to the client.
     * 
     * @param cookie
     *   the cookie to be added to the client.
     */
    public static void setCookie(Cookie cookie) {
        getContext().response.addCookie(cookie);
    }

    /**
     * Returns the header with the given name.
     * 
     * @param name
     *   the name of the header.
     * @return 
     *   the value of the header.
     */
    public static Object getHeader(String name) {
        return getContext().request.getHeader(name);
    }

    // TODO: add other header-related methods...

    /**
     * Encodes the given URL; ths URL is not prefixed with the current context
     * path, and is therefore considered as absolute. An example of such URLs is
     * <code>/MyApplication/myServlet</code>.
     * 
     * @param url
     *   the absolute URL to be encoded.
     * @return 
     *   the URL, in encoded form.
     */
    public static String encodeAbsoluteURL(String url) {
        String encoded = getContext().response.encodeURL(url);
        logger.trace("url '{}' encoded as '{}'", url, encoded);
        return encoded;
    }

    /**
     * Encodes the given URL; the URL is prefixed with the current context path,
     * and is therefore considered as relative to it. An example of such URLs is
     * <code>/css/myStyleSheet.css</code>.
     * 
     * @param url
     *   the relative URL to be encoded.
     * @return 
     *   the URL, in encoded form.
     */
    public static String encodeRelativeURL(String url) {
        String unencoded = getContext().request.getContextPath() + url;
        String encoded = getContext().response.encodeURL(unencoded);
        logger.trace("url '{}' encoded as '{}'", unencoded, encoded);
        return encoded;
    }

    /**
     * Redirects to a different URL, with no referrer URL unless it is specified
     * in the URL itself.
     * 
     * @param url
     *   the URL to redirect the browser to (via a 302 HTTP status response).
     * @throws IOException
     *   if the redirect operation fails.
     */
    public static void sendRedirect(String url) throws IOException {
        getContext().response.sendRedirect(url);
    }

    /**
     * Sends an error code to the client.
     * 
     * @param error
     *   the error code (e.g. "401 Unauthorized").
     * @throws IOException
     *   if the redirect operation fails.
     */
    public static void sendError(int error) throws IOException {
        getContext().response.sendError(error);
    }

    // /**
    // * Returns the resource bundle associated with the underlying portlet, for
    // * the given locale.
    // *
    // * @param locale
    // * the selected locale.
    // * @return
    // * the portlet's configured resource bundle.
    // */
    // public static ResourceBundle getResouceBundle(Locale locale) {
    // return getContext().filter.getResourceBundle(locale);
    // }

    /**
     * Returns an enumeration value representing the current HTTP method.
     * 
     * @return
     *   an enumeration value representing the current HTTP method.
     */
    public static HttpMethod getHttpMethod() {
        return HttpMethod.fromString(getContext().request.getMethod());
    }

    /**
     * Returns whether the request is a multipart/form-data request.
     * 
     * @return
     *   whether the request is a multipart/form-data request.
     */
    public static boolean isMultiPartRequest() {
        return getContext().parts != null;
    }

    /**
     * Checks if the give scope contains a non-null value under the given name.
     * 
     * @param key
     *   the name of the value.
     * @param scope
     *   the scope in which it should be located.
     * @return 
     *   whether the given scope contains the value.
     * @throws WebMVCException
     */
    public static boolean hasValue(String key, Scope scope) throws WebMVCException {
        boolean result = false;
        if (!Strings.isValid(key)) {
            logger.error("value name must be valid");
            throw new WebMVCException("Value name must be valid.");
        }
        switch (scope) {
        case FORM:
            if (isMultiPartRequest()) {
                result = getContext().parts.containsKey(key);
            } else {
                result = getContext().request.getParameterValues(key) != null;
            }
            break;
        case REQUEST:
            result = getContext().request.getAttribute(key) != null;
            break;
        case CONVERSATION:
            String conversationId = Conversation.getConversationId(key);
            String valueId = Conversation.getValueId(key);
            if (Strings.areValid(conversationId, valueId)) {
                logger.trace("checking existence of value '{}' in conversation '{}'", valueId, conversationId);
                @SuppressWarnings("unchecked")
                Map<String, Map<String, Object>> conversations = (Map<String, Map<String, Object>>) getValue(
                        CONVERSATION_SCOPED_ATTRIBUTES_KEY, Scope.SESSION);
                if (conversations != null && conversations.get(conversationId) != null) {
                    result = conversations.get(conversationId).containsKey(valueId);
                }
            }
            break;
        case SESSION:
            result = getContext().request.getSession().getAttribute(key) != null;
            break;
        case STICKY:
            String user = getRemoteUser();
            if (Strings.isValid(user)) {
                @SuppressWarnings("unchecked")
                Map<String, Map<String, Object>> sticky = (Map<String, Map<String, Object>>) getValue(
                        STICKY_SCOPED_ATTRIBUTES_KEY, Scope.APPLICATION);
                if (sticky != null && sticky.get(user) != null) {
                    result = sticky.get(user).containsKey(key);
                }
            }
            break;
        case APPLICATION:
            result = getContext().filter.getServletContext().getAttribute(key) != null;
            break;
        case CONFIGURATION:
            if (getContext().configuration != null) {
                result = getContext().configuration.get(key) != null;
            }
            break;
        case SYSTEM:
            result = Strings.isValid(System.getProperty(key));
            break;
        case ENVIRONMENT:
            result = Strings.isValid(System.getenv(key));
            break;

        }
        logger.debug("scope '{}' {} value '{}'", scope.name(), result ? "contains" : "doesn't contain", key);
        return result;
    }

    /**
     * Returns the value associated with the given name if present in the given
     * scope; no difference is made between parameters and attributes, in order
     * to provide a consistent high-level view of parameter and attribute
     * passing.
     * 
     * @param key
     *   the name of the value.
     * @param scope
     *   the scope in which the value should be looked up.
     * @return 
     *   the requested parameter or attribute value, or null if not found.
     */
    public static Object getValue(String key, Scope scope) throws WebMVCException {
        if (!Strings.isValid(key)) {
            logger.error("value name must be valid");
            throw new WebMVCException("Value name must be valid.");
        }
        Object value = null;
        switch (scope) {
        case FORM:
            if (isMultiPartRequest()) {
                FileItem item = getContext().parts.get(key);
                if (item != null) {
                    if (item.isFormField()) {
                        value = item.getString();
                    } else {
                        value = new UploadedFile(item);
                    }
                }
            } else {
                value = getContext().request.getParameterValues(key);
            }
            break;
        case REQUEST:
            value = getContext().request.getAttribute(key);
            break;
        case CONVERSATION:
            String conversationId = Conversation.getConversationId(key);
            String valueId = Conversation.getValueId(key);
            if (Strings.areValid(conversationId, valueId)) {
                //            logger.trace("retrieving value '{}' in conversation '{}'", valueId, conversationId);
                @SuppressWarnings("unchecked")
                Map<String, Map<String, Object>> conversations = (Map<String, Map<String, Object>>) getValue(
                        CONVERSATION_SCOPED_ATTRIBUTES_KEY, Scope.SESSION);
                if (conversations != null && conversations.get(conversationId) != null) {
                    value = conversations.get(conversationId).get(valueId);
                }
            }
            break;
        case SESSION:
            value = getContext().request.getSession().getAttribute(key);
            break;
        case STICKY:
            String user = getRemoteUser();
            if (Strings.isValid(user)) {
                @SuppressWarnings("unchecked")
                Map<String, Map<String, Object>> sticky = (Map<String, Map<String, Object>>) getValue(
                        STICKY_SCOPED_ATTRIBUTES_KEY, Scope.APPLICATION);
                if (sticky != null && sticky.get(user) != null) {
                    value = sticky.get(user).get(key);
                }
            }
            break;
        case APPLICATION:
            value = getContext().filter.getServletContext().getAttribute(key);
            break;
        case CONFIGURATION:
            if (getContext().configuration != null) {
                value = getContext().configuration.get(key);
            }
            break;
        case SYSTEM:
            value = System.getProperty(key);
            break;
        case ENVIRONMENT:
            value = System.getenv(key);
            break;
        }
        //      logger.trace("value '{}' in scope '{}' has value '{}' (class {})", key, scope.name(), value, value != null ? value.getClass().getSimpleName() : "n.a.");
        return value;
    }

    /**
     * Returns a copy of the map of values at the given scope.
     * 
     * @param scope
     *   the scope whose values are to be returned.
     * @return 
     *   a copy of the map of attributes/parameters at the requested scope.
     * @throws WebMVCException
     */
    public static Map<String, Object> getValues(Scope scope) throws WebMVCException {
        return getValues(scope, null);
    }

    /**
     * Returns a copy of the map of values at the given scope, possibly applying
     * a filter to value names according to the provided pattern.
     * 
     * @param scope
     *   the scope whose values are to be returned.
     * @param pattern
     *   an optional regular expression to return only matching values.
     * @return 
     *   a copy of the map of attributes/parameters at the requested scope.
     * @throws WebMVCException
     */
    public static Map<String, Object> getValues(Scope scope, Regex pattern) throws WebMVCException {
        Set<String> names = getValueNames(scope);
        Map<String, Object> map = new HashMap<>();
        for (String name : names) {
            map.put(name, getValue(name, scope));
        }
        return map;
    }

    /**
     * Sets the value associated with the given name into the given scope;
     * despite making no difference between parameters and attributes from a
     * theoretical standpoint, this method will actually perform a check to see
     * if an attempt is being made to store a value in a read-only context, so
     * make sure you do not try to set a value in FORM or CONFIGURATION scopes
     * as this would result in an error at runtime.
     * 
     * @param key
     *   the name of the value.
     * @param value
     *   the value to be stored.
     * @param scope
     *   the scope into which the value should be stored.
     */
    @SuppressWarnings("unchecked")
    public static void setValue(String key, Object value, Scope scope) throws WebMVCException {
        if (!Strings.isValid(key)) {
            logger.error("value name must be valid");
            throw new WebMVCException("Value name must be valid.");
        }
        if (scope.isReadOnly()) {
            logger.error("trying to store value in read-only scope '{}'", scope.name());
            throw new WebMVCException("Trying to store value in read-only scope '" + scope.name() + "'.");
        }

        Map<String, Object> map = null;
        switch (scope) {
        case REQUEST:
            getContext().request.setAttribute(key, value);
            break;
        case CONVERSATION:
            String conversationId = Conversation.getConversationId(key);
            String valueId = Conversation.getValueId(key);
            if (Strings.areValid(conversationId, valueId)) {
                logger.trace("setting value '{}' in conversation '{}'", valueId, conversationId);
                synchronized (ActionContext.class) {
                    Map<String, Map<String, Object>> conversations = (Map<String, Map<String, Object>>) getValue(
                            CONVERSATION_SCOPED_ATTRIBUTES_KEY, Scope.SESSION);
                    if (conversations == null) {
                        conversations = Collections.synchronizedMap(new HashMap<String, Map<String, Object>>());
                        setValue(CONVERSATION_SCOPED_ATTRIBUTES_KEY, conversations, Scope.SESSION);
                    }
                    map = conversations.get(conversationId);
                    if (map == null) {
                        map = new HashMap<>();
                        conversations.put(conversationId, map);
                    }
                    map.put(valueId, value);
                }
            }
            break;
        case SESSION:
            getContext().request.getSession().setAttribute(key, value);
            break;
        case STICKY:
            String user = getRemoteUser();
            if (Strings.isValid(user)) {
                user = user.trim();
                synchronized (ActionContext.class) {
                    Map<String, Map<String, Object>> sticky = (Map<String, Map<String, Object>>) getValue(
                            STICKY_SCOPED_ATTRIBUTES_KEY, Scope.APPLICATION);
                    if (sticky == null) {
                        sticky = Collections.synchronizedMap(new HashMap<String, Map<String, Object>>());
                        setValue(STICKY_SCOPED_ATTRIBUTES_KEY, sticky, Scope.APPLICATION);
                    }
                    map = sticky.get(user);
                    if (map == null) {
                        map = new HashMap<>();
                        sticky.put(user, map);
                    }
                    map.put(key, value);
                }
            }
            break;
        case APPLICATION:
            getContext().filter.getServletContext().setAttribute(key, value);
            break;
        default:
            logger.error("should never get here: is this a bug?");
        }
        logger.debug("value '{}' in scope '{}' set to value '{}' (class {})", key, scope.name(), value,
                value != null ? value.getClass().getSimpleName() : "n.a.");
    }

    /**
     * Adds all the entries in the given map into the given scope, overriding
     * any existing values with the same names.
     * 
     * @param values
     *   a map of values.
     * @param scope
     *   the scope into which values should be added.
     * @throws WebMVCException
     */
    public static void setValues(Map<String, Object> values, Scope scope) throws WebMVCException {
        setValues(values, scope, true);
    }

    /**
     * Adds all the entries in the given map into the given scope.
     * 
     * @param values
     *   a map of values.
     * @param scope
     *   the scope into which values should be added.
     * @param override
     *   if {@code true} (the default), the value will always be added to scope,
     *   thus overriding any existing value; if {@code false} the value will be
     *   added only if not already present.
     * @throws WebMVCException
     */
    public static void setValues(Map<String, Object> values, Scope scope, boolean override) throws WebMVCException {
        if (values == null) {
            logger.error("input values map must not be null");
            throw new WebMVCException("Input value map must not be null.");
        }
        for (Entry<String, Object> entry : values.entrySet()) {
            if (override || !hasValue(entry.getKey(), scope)) {
                setValue(entry.getKey(), entry.getValue(), scope);
            }
        }
    }

    /**
     * Removes the value associated with the given name from the given scope;
     * despite making no difference between parameters and attributes from a
     * theoretical standpoint, this method will actually perform a check to see
     * if an attempt is being made to remove a value from a read-only context,
     * so make sure you do not try to remove a value from FORM or CONFIGURATION
     * scopes as this would result in an error at runtime.
     * 
     * @param key
     *   the name of the value.
     * @param scope
     *   the scope from which the value should be removed.
     */
    @SuppressWarnings("unchecked")
    public static void removeValue(String key, Scope scope) throws WebMVCException {

        if (!Strings.isValid(key)) {
            logger.error("value name must be valid");
            throw new WebMVCException("Value name must be valid.");
        }
        if (scope.isReadOnly()) {
            logger.error("trying to remove value from read-only scope '{}'", scope.name());
            throw new WebMVCException("Trying to remove value from read-only scope '" + scope.name() + "'.");
        }

        switch (scope) {
        case REQUEST:
            getContext().request.removeAttribute(key);
            break;
        case CONVERSATION:
            String conversationId = Conversation.getConversationId(key);
            String valueId = Conversation.getValueId(key);
            if (Strings.areValid(conversationId, valueId)) {
                logger.trace("retrieving value '{}' in conversation '{}'", valueId, conversationId);
                Map<String, Map<String, Object>> conversations = (Map<String, Map<String, Object>>) getValue(
                        CONVERSATION_SCOPED_ATTRIBUTES_KEY, Scope.SESSION);
                if (conversations != null && conversations.get(conversationId) != null) {
                    conversations.get(conversationId).remove(valueId);
                }
            }
            break;
        case SESSION:
            getContext().request.getSession().removeAttribute(key);
            break;
        case STICKY:
            String user = getRemoteUser();
            if (Strings.isValid(user)) {
                Map<String, Map<String, Object>> sticky = (Map<String, Map<String, Object>>) getValue(
                        STICKY_SCOPED_ATTRIBUTES_KEY, Scope.APPLICATION);
                if (sticky != null && sticky.get(user) != null) {
                    sticky.get(user).remove(key);
                }
            }
            break;
        case APPLICATION:
            getContext().filter.getServletContext().removeAttribute(key);
            break;
        default:
            logger.error("should never get here: is this a bug?");
        }
        logger.debug("value '{}' removed from scope '{}'", key, scope.name());
    }

    /**
     * Removes all values in the input set from the given scope.
     * 
     * @param names
     *   a set of value names.
     * @param scope
     *   the scope from which the values should be removed.
     * @throws WebMVCException
     */
    public static void removeValues(Set<String> names, Scope scope) throws WebMVCException {
        if (names == null) {
            logger.error("set of value names must not be null");
            throw new WebMVCException("Set of value names must not be null.");
        }

        for (String name : names) {
            removeValue(name, scope);
        }
    }

    /**
     * Removes all values whose name matches the given regular expression from
     * the given scope.
     * 
     * @param pattern
     *   a regular expression against which value names are matched.
     * @param scope
     *   the scope from which to remove values.
     * @throws WebMVCException
     */
    public static void removeValues(String pattern, Scope scope) throws WebMVCException {
        if (pattern == null) {
            logger.error("regular expression to match against value names must not be null");
            throw new WebMVCException("Regular expression to match against value names must not be null.");
        }
        Set<String> names = getValueNames(pattern, scope);
        removeValues(names, scope);
    }

    /**
     * Removes all values from the given scope.
     * 
     * @param scope
     *   the scope from which values must be cleared.
     * @throws WebMVCException
     */
    public static void clearValues(Scope scope) throws WebMVCException {
        switch (scope) {
        case CONVERSATION:
            removeValues(".*:.*", scope);
            break;
        default:
            removeValues(".*", scope);
            break;
        }
    }

    /**
     * Retrieves the names of attributes and parameters in the given scope.
     * 
     * @param scope
     *   the scope whose value (attribute/parameter) names should be retrieved.
     * @return 
     *   the names of the attributes/parameters in the given scope.
     * @throws WebMVCException 
     */
    public static Set<String> getValueNames(Scope scope) throws WebMVCException {
        return getValueNames(null, scope);
    }

    /**
     * Retrieves the names of attributes and parameters in the given scope,
     * possibly filtering out those that do not match the given pattern (if
     * provided).
     * 
     * @param pattern
     *   an optional regular expression: only names matching it will be returned.
     * @param scope
     *   the scope whose value (attribute/parameter) names should be retrieved.
     * @return 
     *   the names of the attributes/parameters in the given scope.
     * @throws WebMVCException 
     */
    @SuppressWarnings("unchecked")
    public static Set<String> getValueNames(String pattern, Scope scope) throws WebMVCException {
        Set<String> names = new HashSet<>();
        Enumeration<?> enumeration = null;
        Regex regex = null;
        if (Strings.isValid(pattern)) {
            regex = new Regex(pattern);
        }
        switch (scope) {
        case FORM:
            if (isMultiPartRequest()) {
                names.addAll(getContext().parts.keySet());
            } else {
                enumeration = getContext().request.getParameterNames();
                while (enumeration.hasMoreElements()) {
                    String name = (String) enumeration.nextElement();
                    if (regex == null || regex.matches(name)) {
                        names.add(name);
                    }
                }
            }
            break;
        case REQUEST:
            enumeration = getContext().request.getAttributeNames();
            while (enumeration.hasMoreElements()) {
                String name = (String) enumeration.nextElement();
                if (regex == null || regex.matches(name)) {
                    names.add(name);
                }
            }
            break;
        case CONVERSATION:
            Regex conversation = Strings.isValid(Conversation.getConversationId(pattern))
                    ? new Regex(Conversation.getConversationId(pattern))
                    : new Regex(".*");
            regex = Strings.isValid(Conversation.getValueId(pattern)) ? new Regex(Conversation.getValueId(pattern))
                    : new Regex(".*");
            Map<String, Map<String, Object>> conversations = (Map<String, Map<String, Object>>) getValue(
                    CONVERSATION_SCOPED_ATTRIBUTES_KEY, Scope.SESSION);
            if (conversations != null) {
                for (String conversationId : conversations.keySet()) {
                    if (conversation.matches(conversationId)) {
                        for (String name : conversations.get(conversationId).keySet()) {
                            if (regex.matches(name)) {
                                names.add(conversationId + ":" + name);
                            }
                        }
                    }
                }
            }
            break;
        case SESSION:
            enumeration = getContext().request.getSession().getAttributeNames();
            while (enumeration.hasMoreElements()) {
                String name = (String) enumeration.nextElement();
                if (regex == null || regex.matches(name)) {
                    names.add(name);
                }
            }
            break;
        case STICKY:
            String user = getRemoteUser();
            if (Strings.isValid(user)) {
                Map<String, Map<String, Object>> sticky = (Map<String, Map<String, Object>>) getValue(
                        STICKY_SCOPED_ATTRIBUTES_KEY, Scope.APPLICATION);
                if (sticky != null && sticky.get(user) != null) {
                    for (String name : sticky.get(user).keySet()) {
                        if (regex == null || regex.matches(name)) {
                            names.add(name);
                        }
                    }
                }
            }
            break;
        case APPLICATION:
            enumeration = getContext().filter.getServletContext().getAttributeNames();
            while (enumeration.hasMoreElements()) {
                String name = (String) enumeration.nextElement();
                if (regex == null || regex.matches(name)) {
                    names.add(name);
                }
            }
            break;
        case CONFIGURATION:
            if (getContext().configuration != null) {
                for (String name : getContext().configuration.getKeys()) {
                    if (regex == null || regex.matches(name)) {
                        names.add(name);
                    }
                }
            }
            break;
        case SYSTEM:
            enumeration = System.getProperties().keys();
            while (enumeration.hasMoreElements()) {
                String name = (String) enumeration.nextElement();
                if (regex == null || regex.matches(name)) {
                    names.add(name);
                }
            }
            break;
        case ENVIRONMENT:
            for (String name : System.getenv().keySet()) {
                if (regex == null || regex.matches(name)) {
                    names.add(name);
                }
            }
            break;
        }
        return names;
    }

    /**
     * Looks for a value in any of the provided scopes, in the given order.
     * 
     * @param name
     *   the name of the parameter to look for.
     * @param scopes
     *   the ordered list of scopes to look into.
     * @return 
     *   the value, as soon as it is found; null otherwise.
     * @throws WebMVCException
     */
    public static Object findValue(String name, Scope... scopes) throws WebMVCException {
        Object value = null;
        for (Scope scope : scopes) {
            if (hasValue(name, scope)) {
                value = getValue(name, scope);
                break;
            }
        }
        return value;
    }

    /**
     * Looks up any value whose name matches the given regular expression in the
     * given set of scopes.
     * 
     * @param pattern
     *   a pattern to match against value names, as a string.
     * @param scopes
     *   a set of scopes to look into.
     * @return
     *   a map containing all the values whose names match the given pattern in 
     *   the given scopes.
     * @throws WebMVCException
     */
    public static Map<String, Object> matchValues(String pattern, Scope... scopes) throws WebMVCException {
        if (!Strings.isValid(pattern)) {
            logger.error("regular expression to match against value names must be a valid string");
            throw new WebMVCException("Regular expression to match against value names must be a valid string.");
        }
        return matchValues(new Regex(pattern), scopes);
    }

    /**
     * Looks up any value whose name matches the given regular expression in the
     * given set of scopes.
     * 
     * @param pattern
     *   a pattern to match against value names.
     * @param scopes
     *   a set of scopes to look into.
     * @return
     *   a map containing all the values whose names match the given pattern in 
     *   the given scopes.
     * @throws WebMVCException
     */
    public static Map<String, Object> matchValues(Regex pattern, Scope... scopes) throws WebMVCException {
        if (pattern == null) {
            logger.error("regular expression to match against value names must not be null");
            throw new WebMVCException("Regular expression to match against value names must not be null.");
        }
        Map<String, Object> values = new HashMap<>();

        if (scopes != null && scopes.length > 0) {
            // visit the scopes in reverse order so that first scopes have
            // higher
            // priority in retrieving values than last ones
            for (int i = scopes.length - 1; i >= 0; i--) {
                values.putAll(getValues(scopes[i], pattern));
            }
        }
        return values;
    }

    /**
     * Sets interceptor-specific data into the action context; this information
     * is available through different calls and can be used to keep track of
     * system status, such as number of calls for target, or number of accesses
     * by the same user etc. This method should only be used by interceptors,
     * and the associated data should not be tampered with, to avoid
     * unpredictable behaviour.
     * 
     * @param interceptorId
     *   the namepaced id of the interceptor this data belongs to (see
     *   {@link Interceptor#getId()} for details).
     * @param data
     *   the data object.
     * @throws WebMVCException
     */
    public static void setInterceptorData(String interceptorId, Object data) throws WebMVCException {
        @SuppressWarnings("unchecked")
        Map<String, Object> map = (Map<String, Object>) getValue(INTERCEPTOR_DATA_KEY, Scope.SESSION);
        if (map == null) {
            map = Collections.synchronizedMap(new HashMap<String, Object>());
            setValue(INTERCEPTOR_DATA_KEY, map, Scope.SESSION);
        }
        map.put(interceptorId, data);
    }

    /**
     * Retrieves interceptor-specific data stored by the given interceptor. This
     * method should only be used by interceptors, and the associated data
     * should not be tampered with, to avoid unpredictable behaviour.
     * 
     * @param interceptorId
     *   the namespace id of the interceptor owning the stored data.
     * @return 
     *   the data, or null if none found.
     * @throws WebMVCException
     */
    public static Object getInterceptorData(String interceptorId) throws WebMVCException {
        @SuppressWarnings("unchecked")
        Map<String, Object> map = (Map<String, Object>) getValue(INTERCEPTOR_DATA_KEY, Scope.SESSION);
        Object data = null;
        if (map != null) {
            data = map.get(interceptorId);
        }
        return data;
    }

    /**
     * Returns the data stored by the given interceptor. This method should only
     * be used by interceptors, and the associated data should not be tampered
     * with, to avoid unpredictable behaviour.
     * 
     * @param interceptorId
     *   the namespace id of the interceptor requesting the data.
     * @param clazz
     *   the type of the data to be retrieved, so it can be automatically cast.
     * @return 
     *   the data, already cast to the given type, or null if nothing found.
     * @throws WebMVCException
     */
    public static <T> T getInterceptorData(String interceptorId, Class<? extends T> clazz) throws WebMVCException {
        Object data = getInterceptorData(interceptorId);
        return data != null ? clazz.cast(data) : null;
    }

    /**
     * Returns the underlying request object.
     * 
     * @return 
     *   the underlying request object.
     */
    @Deprecated
    public static HttpServletRequest getRequest() {
        return getContext().request;
    }

    /**
     * Returns the underlying response object.
     * 
     * @return 
     *   the underlying response object.
     */
    @Deprecated
    public static HttpServletResponse getResponse() {
        return getContext().response;
    }

    /**
     * Returns the underlying session object.
     * 
     * @return 
     *   the underlying session object.
     */
    @Deprecated
    public static HttpSession getSession() {
        return getContext().request.getSession();
    }
}