org.jessma.mvc.ActionDispatcher.java Source code

Java tutorial

Introduction

Here is the source code for org.jessma.mvc.ActionDispatcher.java

Source

/*
 * Copyright Bruce Liang (ldcsaa@gmail.com)
 *
 * Version   : JessMA 3.5.1
 * Author   : Bruce Liang
 * Website   : http://www.jessma.org
 * Project   : http://www.oschina.net/p/portal-basic
 * Blog      : http://www.cnblogs.com/ldcsaa
 * WeiBo   : http://weibo.com/u/1402935851
 * QQ Group   : 75375912
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jessma.mvc;

import java.beans.PropertyDescriptor;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.jessma.mvc.Action.ResultType;
import org.jessma.util.BeanHelper;
import org.jessma.util.GeneralHelper;
import org.jessma.util.http.HttpHelper;

/**
 * 
 * MVC ? {@link Filter}
 *
 */
/*
@WebFilter   (
        filterName="ActionDispatcher",
        urlPatterns={"*.action"},
        asyncSupported=false,
        dispatcherTypes={
                       DispatcherType.ASYNC,
                       DispatcherType.ERROR,
                       DispatcherType.FORWARD,
                       DispatcherType.INCLUDE,
                       DispatcherType.REQUEST
                    }
     )
*/
public class ActionDispatcher implements Filter {
    static final String PATH_SEPARATOR = HttpHelper.URL_PATH_SEPARATOR;
    static final String CURRENT_PATH_PREFIX = "./";

    private static final String GLOBAL_KEY = "global";
    private static final String INCLUDE_KEY = "include";
    private static final String INCLUDE_FILE_KEY = "file";
    private static final String ACTIONS_KEY = "actions";
    private static final String ACTIONS_PATH_KEY = "path";
    private static final String ACTION_ENTRY_SEPARATOR = "!";
    private static final String ACTION_KEY = "action";
    private static final String ACTION_NAME_KEY = "name";
    private static final String ACTION_CLASS_KEY = "class";
    private static final String ACTION_ENTRY_KEY = "entry";
    private static final String ACTION_ENTRY_NAME_KEY = "name";
    private static final String ACTION_ENTRY_METHOD_KEY = "method";
    private static final String RESULT_KEY = "result";
    private static final String RESULT_TYPE_KEY = "type";
    private static final String RESULT_NAME_KEY = "name";
    private static final String ACTION_DEFAULT_ENTRY_METHOD = "execute";

    private static final String CONFIG_FILE_KEY = "mvc-config-file";
    private static final String DEFAULT_CONFIG_FILE = "mvc-config.xml";

    private static final String ACTION_SUFFIX_KEY = "action-suffix";
    private static final String BASE_PATH_KEY = "base-path";
    private static final String BASE_PATH_TYPE_KEY = "type";
    private static final String BASE_PATH_HREF_KEY = "href";
    private static final String SUFFIX_CHARACTER = ".";
    private static final String DEFAULT_ACTION_SUFFIX = ".action";

    private static final String I18N_KEY = "i18n";
    private static final String I18N_DEF_LOCALE_KEY = "default-locale";
    private static final String I18N_DEF_BUNDLE_KEY = "default-bundle";

    private static final String BEAN_VLD_KEY = "bean-validation";
    private static final String BEAN_VLD_ENABLE_KEY = "enable";
    private static final String BEAN_VLD_BUNDLE_KEY = "bundle";
    private static final String BEAN_VLD_VALIDATOR_KEY = "validator";
    private static final String BEAN_VLD_DEFAULT_VALIDATOR = "org.jessma.mvc.validation.HibernateBeanValidator";

    private static final String ACTION_FILTERS_KEY = "action-filters";
    private static final String FILTER_KEY = "filter";
    private static final String FILTER_CLASS_KEY = "class";
    private static final String FILTER_PATTERN_KEY = "pattern";
    private static final String FILTER_METHODS_KEY = "methods";
    private static final String FILTER_DEFAULT_PATTERN = ".*";
    private static final String FILTER_DEFAULT_METHODS = ".*";

    private static final String ACTION_CONV_KEY = "action-convention";
    private static final String CONV_ENABLE_KEY = "enable";
    private static final String CONV_DETECT_PHYSICAL_FILE_KEY = "detect-physical-file";
    private static final String CONV_BASE_PACKAGE_KEY = "action-base-package";
    private static final String CONV_DISPATCH_FILE_PATH_KEY = "dispatch-file-path";
    private static final String CONV_DISPATCH_FILE_TYPE_KEY = "dispatch-file-type";
    private static final String CONV_PHYSICAL_FILE_PATH_KEY = "physical-file-path";
    private static final String CONV_FILE_NAME_SEPARATOR_KEY = "file-name-separator";

    private static final String CONV_DEFAULT_DISPATCH_FILE_PATH = "/WEB-INF/page";
    private static final String CONV_DEFAULT_FILE_TYPE = "jsp";
    private static final String CONV_DEFAULT_FILE_NAME_SEPARATOR = "_";
    private static final String CONV_ACTION_PATH_NAME_SEPARATOR = "-";
    private static final String CONV_ACTION_NAME_USUAL_ENDING = "Action";

    private static final String RESULT_PATH_ALIASES_KEY = "result-path-aliases";
    private static final String RESULT_PATH_ALIAS_KEY = "alias";
    private static final String RESULT_PATH_ALIAS_NAME_KEY = "name";
    private static final String RESULT_PATH_ALIAS_PATH_KEY = "path";
    private static final String RESULT_PATH_PLACEHOLDER_BEGIN = "${";
    private static final String RESULT_PATH_PLACEHOLDER_END = "}";

    private static final String GLOBAL_RESULTS_KEY = "global-results";
    private static final String GLOBAL_EXCEPTION_MAPS_KEY = "global-exception-mappings";

    private static final String ENCODING_KEY = "encoding";

    private static final String EXCEPTION_MAP_KEY = "exception-mapping";
    private static final String EXCEPTION_KEY = "exception";

    private String encoding;
    private String actionSuffix;
    private BasePathConfig basePath;
    private I18nConfig i18nCfg;
    private BeanValidation validation;
    private ActionConvention convention;
    private Map<String, ActionResult> globalResults;
    private List<ActionException> globalExceptions;
    private List<ActionFilterInfo> filterInfoList;
    private LinkedList<ActionFilter> filterList;
    private Map<String, Map<String, ActionEntryConfig>> actionPkgMap;
    private Map<Method, LinkedList<ActionFilter>> filterCache;
    private Map<Class<ActionFilter>, ActionFilter> filterMap;
    private Map<Class<? extends Action>, ActionConfig> convACMap;
    private Map<String, String> resultAliasMap;

    private FilterConfig filterCfg;
    private ServletContext context;
    private boolean pausing;

    private static ActionDispatcher instance;

    /* **************************************************************************************************** */
    //                                 Business Process                                  //
    /* **************************************************************************************************** */

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        filterCfg = filterConfig;
        context = filterConfig.getServletContext();

        attachInstance();
        loadConfig();
    }

    private void loadConfig() throws ServletException {
        String confFile = filterCfg.getInitParameter(CONFIG_FILE_KEY);

        if (GeneralHelper.isStrEmpty(confFile))
            confFile = DEFAULT_CONFIG_FILE;

        confFile = GeneralHelper.getClassResourcePath(ActionDispatcher.class, confFile);

        resetProperties();
        loadConfigFile(confFile, true, null);
        loadActionFilters();

        if (i18nCfg.defaultLocale != null)
            Locale.setDefault(i18nCfg.defaultLocale);

        ActionSupport.setBeanValidation(validation);

        context.setAttribute(Action.Constant.APP_ATTR_DEFAULT_APP_BUNDLE, i18nCfg.defaultBundle);
        context.setAttribute(Action.Constant.APP_ATTR_DEFAULT_VLD_BUNDLE, validation.bundle);
        context.setAttribute(Action.Constant.APP_ATTR_CONTEXT_PATH, context.getContextPath() + PATH_SEPARATOR);
        context.setAttribute(Action.Constant.APP_ATTR_BASE_TYPE, basePath.baseType);

        if (basePath.baseType == Action.BaseType.MANUAL)
            context.setAttribute(Action.Constant.APP_ATTR_BASE_PATH, basePath.baseHref);
        else
            context.removeAttribute(Action.Constant.APP_ATTR_BASE_PATH);
    }

    private void resetProperties() {
        encoding = null;
        actionSuffix = DEFAULT_ACTION_SUFFIX;
        basePath = new BasePathConfig();
        i18nCfg = new I18nConfig();
        validation = new BeanValidation();
        convention = new ActionConvention();
        globalResults = null;
        globalExceptions = null;
        filterInfoList = new ArrayList<ActionFilterInfo>();
        filterList = new LinkedList<ActionFilter>();
        actionPkgMap = new HashMap<String, Map<String, ActionEntryConfig>>();
        filterCache = new HashMap<Method, LinkedList<ActionFilter>>();
        filterMap = new HashMap<Class<ActionFilter>, ActionFilter>();
        convACMap = new HashMap<Class<? extends Action>, ActionConfig>();
        resultAliasMap = new HashMap<String, String>();
    }

    private void loadConfigFile(String confFile, boolean isMainConfigFile, Set<String> incFiles)
            throws ServletException {
        try {
            SAXReader sr = new SAXReader();
            Document doc = sr.read(new File(confFile));
            Element root = doc.getRootElement();

            if (isMainConfigFile) {
                incFiles = new HashSet<String>();
                incFiles.add(confFile);

                parseGlobal(root);
            }

            parseInclude(root, incFiles);
            parseActionPackage(root);
        } catch (Exception e) {
            throw new ServletException("load MVC config fail", e);
        }
    }

    private void parseGlobal(Element root)
            throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        Element global = root.element(GLOBAL_KEY);
        if (global != null) {
            Element enc = global.element(ENCODING_KEY);
            if (enc != null)
                parseEncoding(enc);

            Element acSuffix = global.element(ACTION_SUFFIX_KEY);
            if (acSuffix != null)
                parseActionSuffix(acSuffix);

            Element i18n = global.element(I18N_KEY);
            if (i18n != null)
                parseI8n(i18n);

            Element vld = global.element(BEAN_VLD_KEY);
            if (vld != null)
                parseBeanValidation(vld);

            Element bp = global.element(BASE_PATH_KEY);
            if (bp != null)
                parseBasePath(bp);

            Element acFilters = global.element(ACTION_FILTERS_KEY);
            if (acFilters != null)
                parseActionFilters(acFilters);

            Element rsPathAliases = global.element(RESULT_PATH_ALIASES_KEY);
            if (rsPathAliases != null)
                parseResultPathAliases(rsPathAliases);

            Element acConv = global.element(ACTION_CONV_KEY);
            if (acConv != null)
                parseActionConvention(acConv);

            Element gResults = global.element(GLOBAL_RESULTS_KEY);
            if (gResults != null)
                globalResults = parseResults(gResults);

            Element gExceptionMaps = global.element(GLOBAL_EXCEPTION_MAPS_KEY);
            if (gExceptionMaps != null)
                globalExceptions = parseExceptionLists(gExceptionMaps);
        }

        ensureGlobalResultNone();
    }

    private void parseEncoding(Element enc) {
        String str = enc.getTextTrim();
        if (GeneralHelper.isStrNotEmpty(str))
            encoding = str;
    }

    private void parseActionSuffix(Element acSuffix) {
        String str = acSuffix.getTextTrim();
        if (GeneralHelper.isStrNotEmpty(str)) {
            if (str.startsWith(SUFFIX_CHARACTER))
                actionSuffix = str;
            else
                actionSuffix = SUFFIX_CHARACTER + str;
        }
    }

    private void parseI8n(Element i18n) {
        String dlc = i18n.attributeValue(I18N_DEF_LOCALE_KEY);
        if (GeneralHelper.isStrNotEmpty(dlc)) {
            i18nCfg.defaultLocale = GeneralHelper.getAvailableLocale(dlc);
            if (i18nCfg.defaultLocale == null)
                throw new RuntimeException(String.format("parse i18n fail (invalid default-locale '%s')", dlc));
        }

        String bundle = i18n.attributeValue(I18N_DEF_BUNDLE_KEY);
        if (GeneralHelper.isStrNotEmpty(bundle))
            i18nCfg.defaultBundle = bundle;
    }

    @SuppressWarnings("unchecked")
    private void parseBeanValidation(Element vld)
            throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        String enable = vld.attributeValue(BEAN_VLD_ENABLE_KEY);
        validation.enable = GeneralHelper.str2Boolean(enable, true);

        String bundle = vld.attributeValue(BEAN_VLD_BUNDLE_KEY);
        if (GeneralHelper.isStrNotEmpty(bundle))
            validation.bundle = bundle;

        if (validation.enable) {
            String vldClass = vld.attributeValue(BEAN_VLD_VALIDATOR_KEY);
            if (GeneralHelper.isStrEmpty(vldClass))
                vldClass = BEAN_VLD_DEFAULT_VALIDATOR;

            Class<? extends BeanValidator> clazz = (Class<? extends BeanValidator>) Class.forName(vldClass);
            validation.validator = clazz.newInstance();
            validation.validator.init();
        }
    }

    private void parseBasePath(Element bp) {
        String type = bp.attributeValue(BASE_PATH_TYPE_KEY);
        if (GeneralHelper.isStrNotEmpty(type)) {
            basePath.baseType = Action.BaseType.fromString(type);
            if (basePath.baseType == Action.BaseType.MANUAL) {
                basePath.baseHref = bp.attributeValue(BASE_PATH_HREF_KEY);
                if (GeneralHelper.isStrEmpty(basePath.baseHref))
                    throw new RuntimeException(
                            "parse base path fail ('href' attribute must be set if 'type' = \"manual\")");

                if (!basePath.baseHref.endsWith(PATH_SEPARATOR))
                    basePath.baseHref += PATH_SEPARATOR;
            }
        }
    }

    private void parseActionConvention(Element acConv) {
        String enable = acConv.attributeValue(CONV_ENABLE_KEY);
        String detect = acConv.attributeValue(CONV_DETECT_PHYSICAL_FILE_KEY);
        String basePkg = acConv.attributeValue(CONV_BASE_PACKAGE_KEY);
        String dispatchPath = acConv.attributeValue(CONV_DISPATCH_FILE_PATH_KEY);
        String fileType = acConv.attributeValue(CONV_DISPATCH_FILE_TYPE_KEY);
        String physicalPath = acConv.attributeValue(CONV_PHYSICAL_FILE_PATH_KEY);
        String nameSep = acConv.attributeValue(CONV_FILE_NAME_SEPARATOR_KEY);

        convention.enable = GeneralHelper.str2Boolean(enable, true);
        convention.detect = GeneralHelper.str2Boolean(detect, true);
        convention.basePackage = GeneralHelper.safeString(basePkg);
        convention.dispatchPath = HttpHelper.ensurePath(parseResultPath(dispatchPath),
                CONV_DEFAULT_DISPATCH_FILE_PATH);
        convention.physicalPath = HttpHelper.ensurePath(parseResultPath(physicalPath), convention.dispatchPath);

        if (GeneralHelper.isStrNotEmpty(fileType))
            convention.fileType = fileType;
        else
            convention.fileType = CONV_DEFAULT_FILE_TYPE;

        if (GeneralHelper.isStrNotEmpty(nameSep))
            convention.separator = nameSep.substring(0, 1);
        else
            convention.separator = CONV_DEFAULT_FILE_NAME_SEPARATOR;
    }

    @SuppressWarnings("unchecked")
    private void parseActionFilters(Element acFilters)
            throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        List<Element> filters = acFilters.elements(FILTER_KEY);

        for (Element filter : filters) {
            String clazz = filter.attributeValue(FILTER_CLASS_KEY);
            String pattern = filter.attributeValue(FILTER_PATTERN_KEY);
            String methods = filter.attributeValue(FILTER_METHODS_KEY);

            if (GeneralHelper.isStrEmpty(clazz))
                throw new RuntimeException("parse action filter fail ('class' attribute must be set)");
            if (GeneralHelper.isStrEmpty(pattern))
                pattern = FILTER_DEFAULT_PATTERN;
            if (GeneralHelper.isStrEmpty(methods))
                methods = FILTER_DEFAULT_METHODS;

            ActionFilterInfo info = new ActionFilterInfo();
            info.afClass = (Class<ActionFilter>) Class.forName(clazz);
            info.pattern = Pattern.compile(pattern);
            info.methods = Pattern.compile(methods);

            filterInfoList.add(info);

            ActionFilter f = filterMap.get(info.afClass);

            if (f == null) {
                f = info.afClass.newInstance();
                f.init();

                filterList.addLast(f);
                filterMap.put(info.afClass, f);
            }
        }
    }

    @SuppressWarnings("unchecked")
    private void parseResultPathAliases(Element rsPathAliases) {
        List<Element> aliases = rsPathAliases.elements(RESULT_PATH_ALIAS_KEY);

        for (Element alias : aliases) {
            String name = alias.attributeValue(RESULT_PATH_ALIAS_NAME_KEY);
            String path = alias.attributeValue(RESULT_PATH_ALIAS_PATH_KEY);

            if (GeneralHelper.isStrEmpty(name))
                throw new RuntimeException("parse result path alias fail ('name' attribute must be set)");
            if (path == null)
                throw new RuntimeException("parse result path alias fail ('path' attribute must be set)");

            resultAliasMap.put(name, parseResultPath(path));
        }
    }

    @SuppressWarnings("unchecked")
    private List<ActionException> parseExceptionLists(Element exceptionMaps) throws ClassNotFoundException {
        List<ActionException> exceptions = new ArrayList<ActionException>();
        List<Element> maps = exceptionMaps.elements(EXCEPTION_MAP_KEY);

        for (Element map : maps) {
            ActionException ae = new ActionException();

            String clazz = map.attributeValue(EXCEPTION_KEY);
            ae.result = map.attributeValue(RESULT_KEY);

            if (GeneralHelper.isStrEmpty(clazz))
                ae.exception = Exception.class;
            else
                ae.exception = (Class<? extends Exception>) Class.forName(clazz);

            if (ae.result == null)
                ae.result = Action.EXCEPTION;

            exceptions.add(ae);
        }

        return exceptions.isEmpty() ? null : exceptions;
    }

    private void ensureGlobalResultNone() {
        if (globalResults == null)
            globalResults = new HashMap<String, ActionResult>();

        if (!globalResults.containsKey(Action.NONE))
            globalResults.put(Action.NONE, new ActionResult(Action.NONE, Action.ResultType.FINISH));
    }

    @SuppressWarnings("unchecked")
    private void parseInclude(Element root, Set<String> incFiles) throws ServletException {
        List<Element> includes = root.elements(INCLUDE_KEY);
        for (Element inc : includes) {
            String fileName = inc.attributeValue(INCLUDE_FILE_KEY);
            String incFile = GeneralHelper.getClassResourcePath(ActionDispatcher.class, fileName);

            if (incFile == null)
                throw new ServletException(String.format("include file '%s' not found", fileName));
            else {
                if (incFiles.contains(incFile))
                    continue;
                else
                    incFiles.add(incFile);
            }

            loadConfigFile(incFile, false, incFiles);
        }
    }

    @SuppressWarnings("unchecked")
    private void parseActionPackage(Element root)
            throws ClassNotFoundException, SecurityException, NoSuchMethodException {
        List<Element> acsList = root.elements(ACTIONS_KEY);
        for (Element actions : acsList) {
            String path = actions.attributeValue(ACTIONS_PATH_KEY);
            path = HttpHelper.ensurePath(path, PATH_SEPARATOR);

            Map<String, ActionEntryConfig> actionEntryMap = actionPkgMap.get(path);

            if (actionEntryMap == null) {
                actionEntryMap = new HashMap<String, ActionEntryConfig>();
                actionPkgMap.put(path, actionEntryMap);
            }

            parseAction(actions, actionEntryMap);
        }
    }

    @SuppressWarnings("unchecked")
    private void parseAction(Element actions, Map<String, ActionEntryConfig> actionEntryMap)
            throws ClassNotFoundException, SecurityException, NoSuchMethodException {
        List<Element> acList = actions.elements(ACTION_KEY);
        for (Element action : acList) {
            ActionConfig ac = new ActionConfig();
            ac.name = action.attributeValue(ACTION_NAME_KEY);
            String clazz = action.attributeValue(ACTION_CLASS_KEY);

            if (clazz == null)
                ac.acClass = ActionSupport.class;
            else
                ac.acClass = (Class<? extends Action>) Class.forName(clazz);

            ac.results = parseResults(action);
            ac.exceptions = parseExceptionLists(action);

            parseActionEntry(action, ac, actionEntryMap);
        }
    }

    @SuppressWarnings("unchecked")
    private void parseActionEntry(Element action, ActionConfig ac, Map<String, ActionEntryConfig> actionEntryMap)
            throws SecurityException, NoSuchMethodException, ClassNotFoundException {
        List<Element> aecList = action.elements(ACTION_ENTRY_KEY);
        for (Element entry : aecList) {
            String name = entry.attributeValue(ACTION_ENTRY_NAME_KEY);
            String method = entry.attributeValue(ACTION_ENTRY_METHOD_KEY);

            String entryName = GeneralHelper.isStrEmpty(name) ? ac.name : ac.name + ACTION_ENTRY_SEPARATOR + name;
            String entryMethod = GeneralHelper.isStrEmpty(method)
                    ? (GeneralHelper.isStrEmpty(name) ? ACTION_DEFAULT_ENTRY_METHOD : name)
                    : method;

            Method m = ac.acClass.getMethod(entryMethod);
            if (!String.class.isAssignableFrom(m.getReturnType()) || !BeanHelper.isPublicInstanceMethod(m)) {
                String msg = String.format("invalid action entry method '%s'", m);
                throw new RuntimeException(msg);
            }

            actionEntryMap.put(entryName,
                    new ActionEntryConfig(entryName, m, ac, parseResults(entry), parseExceptionLists(entry)));
        }

        if (actionEntryMap.get(ac.name) == null)
            actionEntryMap.put(ac.name,
                    new ActionEntryConfig(ac.name, ac.acClass.getMethod(ACTION_DEFAULT_ENTRY_METHOD), ac));
    }

    @SuppressWarnings("unchecked")
    private Map<String, ActionResult> parseResults(Element rsElement) {
        Map<String, ActionResult> map = new HashMap<String, ActionResult>();
        List<Element> results = rsElement.elements(RESULT_KEY);

        for (Element result : results) {
            ActionResult ars = new ActionResult();

            ars.name = result.attributeValue(RESULT_NAME_KEY);
            String type = result.attributeValue(RESULT_TYPE_KEY);
            ars.path = parseResultPath(result.getTextTrim());

            if (ars.name == null)
                ars.name = Action.SUCCESS;

            if (type == null || type.equals(Action.ResultType.DEFAULT.toString())) {
                if (!ars.name.equals(Action.NONE))
                    ars.type = Action.ResultType.DISPATCH;
                else
                    ars.type = Action.ResultType.FINISH;
            } else
                ars.type = Action.ResultType.fromString(type);

            map.put(ars.name, ars);
        }

        return map.isEmpty() ? null : map;
    }

    private String parseResultPath(String path) {
        if (GeneralHelper.isStrEmpty(path))
            return path;

        boolean isOK = true;
        int begin = -1;
        int end = -1;
        int length = path.length();
        StringBuilder sb = new StringBuilder();

        do {
            begin = path.indexOf(RESULT_PATH_PLACEHOLDER_BEGIN, end + 1);

            if (begin != -1) {
                sb.append(path.substring(end + 1, begin));
                end = path.indexOf(RESULT_PATH_PLACEHOLDER_END, begin + 2);

                if (end == -1)
                    isOK = false;

                if (isOK) {
                    String name = path.substring(begin + 2, end);
                    String value = resultAliasMap.get(name);

                    if (value != null)
                        sb.append(value);
                    else {
                        String alias = RESULT_PATH_PLACEHOLDER_BEGIN + name + RESULT_PATH_PLACEHOLDER_END;
                        throw new RuntimeException(String.format(
                                "parse result path fail (alias '%s' not found in result path '%s')", alias, path));
                    }
                }
            }
        } while (isOK && begin != -1);

        if (isOK) {
            if (end < length - 1)
                sb.append(path.substring(end + 1));
        } else
            throw new RuntimeException(String.format("parse result path fail (invalid result path '%s')", path));

        return sb.toString();
    }

    private void loadActionFilters() {
        if (!filterInfoList.isEmpty()) {
            Collection<Map<String, ActionEntryConfig>> packages = actionPkgMap.values();

            for (Map<String, ActionEntryConfig> pkg : packages) {
                Collection<ActionEntryConfig> aecs = pkg.values();

                for (ActionEntryConfig aec : aecs)
                    loadActionFilterCache(aec);
            }
        }
    }

    private void loadActionFilterCache(ActionEntryConfig aec) {
        LinkedList<ActionFilter> filters = filterCache.get(aec.method);

        if (filters == null) {
            filters = matchActionFilters(aec);
            tryPutFilterCache(aec.method, filters);
        }
    }

    private LinkedList<ActionFilter> matchActionFilters(ActionEntryConfig aec) {
        Set<ActionFilter> tmpFilterSet = new HashSet<ActionFilter>();
        LinkedList<ActionFilter> filters = new LinkedList<ActionFilter>();

        for (ActionFilterInfo info : filterInfoList) {
            Matcher m = info.pattern.matcher(aec.container.acClass.getName());

            if (m.matches()) {
                m = info.methods.matcher(aec.method.getName());
                if (m.matches()) {
                    ActionFilter filter = filterMap.get(info.afClass);

                    if (!tmpFilterSet.contains(filter)) {
                        filters.add(filter);
                        tmpFilterSet.add(filter);
                    }
                }
            }
        }

        return filters;
    }

    private void tryPutFilterCache(Method method, LinkedList<ActionFilter> filters) {
        if (!filters.isEmpty()) {
            Collection<LinkedList<ActionFilter>> fs = filterCache.values();

            for (LinkedList<ActionFilter> f : fs) {
                if (filters.equals(f)) {
                    filters = f;
                    break;
                }
            }

            GeneralHelper.syncTryPut(filterCache, method, filters);
        }
    }

    @Override
    public void destroy() {
        while (!filterList.isEmpty())
            filterList.removeLast().destroy();

        if (validation.validator != null)
            validation.validator.destroy();

        resultAliasMap = null;
        convACMap = null;
        filterList = null;
        filterInfoList = null;
        filterMap = null;
        filterCache = null;
        actionPkgMap = null;
        globalResults = null;
        globalExceptions = null;

        context = null;
        filterCfg = null;

        detachInstance();
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) resp;

        if (pausing) {
            response.setHeader("Retry-After", Integer.toString(5));
            response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE,
                    "Server is reloading, please retry after a few seconds");
            return;
        }

        if (encoding != null) {
            request.setCharacterEncoding(encoding);
            response.setCharacterEncoding(encoding);
        }

        String reqPath = request.getServletPath();

        if (reqPath.endsWith(actionSuffix)) {
            String actionPath = reqPath.substring(0, reqPath.length() - actionSuffix.length());
            dispatchAction(request, response, new ActionPackage(actionPath));
        } else
            chain.doFilter(request, response);
    }

    private void dispatchAction(HttpServletRequest request, HttpServletResponse response, ActionPackage pkg)
            throws ServletException, IOException {
        ActionEntryConfig aec = null;

        try {
            aec = extractActionEntryConfig(pkg, true);
        } catch (Exception e) {
            String msg = String.format("Extract Action Convention '%s' fail (%s)", pkg, e.getMessage());
            response.sendError(HttpServletResponse.SC_NOT_FOUND, msg);
            return;
        }

        if (aec == null) {
            String msg = String.format("Action Entry '%s' not found", pkg);
            response.sendError(HttpServletResponse.SC_NOT_FOUND, msg);
            return;
        }

        Action action = null;

        try {
            action = createAction(request, response, aec, pkg);
        } catch (Exception e) {
            String msg = String.format("Instantiate Action '%s (%s)' fail", pkg, aec.container.acClass.getName());
            throw new ServletException(msg, e);
        }

        try {
            String result = executeAction(request, response, aec, action);
            dispatchResult(request, response, pkg, aec, action, result);
        } catch (Exception e) {
            throw new ServletException(e);
        }
    }

    void dispatchResult(HttpServletRequest request, HttpServletResponse response, ActionPackage pkg,
            ActionEntryConfig aec, Action action, String result) throws ServletException, IOException {
        ActionResult rs = findResult(request, pkg, aec, result);

        if (rs != null)
            processResult(request, response, pkg, rs, action);
        else {
            String msg = String.format("Result Name '%s' in Action Entry '%s' not found", result, pkg);
            throw new ServletException(msg);
        }
    }

    private ActionEntryConfig extractActionEntryConfig(ActionPackage pkg, boolean firstTime) throws Exception {
        ActionEntryConfig aec = null;
        Map<String, ActionEntryConfig> actionEntryMap = actionPkgMap.get(pkg.path);

        if (actionEntryMap != null)
            aec = actionEntryMap.get(pkg.name);

        if (aec == null & firstTime && convention.enable) {
            extractActionEntryConvention(pkg);
            aec = extractActionEntryConfig(pkg, false);
        }

        return aec;
    }

    private String executeAction(HttpServletRequest request, HttpServletResponse response, ActionEntryConfig aec,
            Action action) throws Exception {
        String result = null;
        LinkedList<ActionFilter> filters = filterCache.get(aec.method);

        try {
            if (filters == null)
                result = ActionExecutor.execute(action, aec.method);
            else {
                ActionExecutor executor = new ActionExecutor(filters, action, aec.method, context, request,
                        response);
                result = executor.invoke();
            }
        } catch (Exception e) {
            request.setAttribute(Action.Constant.REQ_ATTR_EXCEPTION, e);

            if (aec.exceptions != null)
                result = processActionException(request, aec.exceptions, e);
            if (result == null && aec.container.exceptions != null)
                result = processActionException(request, aec.container.exceptions, e);
            if (result == null && globalExceptions != null)
                result = processActionException(request, globalExceptions, e);
            if (result == null)
                throw e;
        }

        return result;
    }

    private String processActionException(HttpServletRequest request, List<ActionException> aes, Exception e) {
        String result = null;

        for (ActionException ae : aes) {
            if (ae.exception.isAssignableFrom(e.getClass())) {
                result = ae.result;
                break;
            }
        }

        return result;
    }

    private Action createAction(HttpServletRequest request, HttpServletResponse response, ActionEntryConfig aec,
            ActionPackage pkg) throws InstantiationException, IllegalAccessException {
        Action action = aec.container.acClass.newInstance();

        action.setServletContext(context);
        action.setRequest(request);
        action.setResponse(response);

        if (action instanceof ActionSupport) {
            ActionSupport asp = (ActionSupport) action;
            asp.setActionDispatcher(this);
            asp.setActionPackage(pkg);
            asp.setActionEntryConfig(aec);
        }

        parseFormBean(aec, action, request.getParameterMap());

        return action;
    }

    private ActionResult findResult(HttpServletRequest request, ActionPackage pkg, ActionEntryConfig aec,
            String result) {
        ActionResult rs = null;

        if (aec.results != null)
            rs = aec.results.get(result);
        if (rs == null && aec.container.results != null)
            rs = aec.container.results.get(result);
        if (rs == null && globalResults != null)
            rs = globalResults.get(result);
        if (rs == null && convention.enable)
            rs = makeConventionResult(result, request, pkg, aec);

        return rs;
    }

    private void processResult(HttpServletRequest request, HttpServletResponse response, ActionPackage pkg,
            ActionResult ars, Action action) throws ServletException, IOException {
        switch (ars.type) {
        case DISPATCH:
            if (basePath.baseType == Action.BaseType.AUTO)
                request.setAttribute(Action.Constant.REQ_ATTR_BASE_PATH, HttpHelper.getRequestBasePath(request));

            request.setAttribute(Action.Constant.REQ_ATTR_ACTION, action);
            RequestDispatcher rd = request.getRequestDispatcher(response.encodeURL(ars.path));

            if (rd != null)
                rd.forward(request, response);
            else {
                String msg = String.format("Dispatch URL '%s' not found", ars.path);
                response.sendError(HttpServletResponse.SC_NOT_FOUND, msg);
            }

            break;
        case REDIRECT:
            response.sendRedirect(response.encodeRedirectURL(ars.path));
            break;
        case CHAIN:
            request.setAttribute(Action.Constant.REQ_ATTR_ACTION, action);
            dispatchChainAction(request, response, pkg, ars.path);
            break;
        case FINISH:
            break;
        default:
            assert false;
        }
    }

    private final <T> void parseFormBean(ActionEntryConfig aec, Action action, Map<String, T> paramMap) {
        if (aec.formBeanAttr != null) {
            Object formBean = null;

            if (aec.formBeanAttr.property != null) {
                formBean = BeanHelper.createBean(aec.formBeanAttr.property.getPropertyType(), paramMap);
                BeanHelper.setProperty(action, aec.formBeanAttr.property, formBean);
            } else if (aec.formBeanAttr.field != null) {
                formBean = BeanHelper.createBean(aec.formBeanAttr.field.getType(), paramMap);
                BeanHelper.setFieldValue(action, aec.formBeanAttr.field, formBean);
            } else {
                formBean = action;
                BeanHelper.setPropertiesOrFieldValues(formBean, paramMap);
            }

            if (action instanceof ActionSupport) {
                ActionSupport asp = (ActionSupport) action;
                asp.setFormBean(formBean);

                if (validation.enable) {
                    asp.setAutoValidation(aec.formBeanAttr.validate);
                    asp.setValidationGroups(aec.formBeanAttr.groups);
                }
            }
        }
    }

    private void dispatchChainAction(HttpServletRequest request, HttpServletResponse response,
            ActionPackage currentPkg, String path) throws ServletException, IOException {
        ActionPackage pkg = new ActionPackage(path, currentPkg.path);
        dispatchAction(request, response, pkg);
    }

    static class ActionEntryConfig {
        @SuppressWarnings("unused")
        private String name;
        private Method method;
        private ActionConfig container;
        private Map<String, ActionResult> results;
        private List<ActionException> exceptions;
        private FormBeanAttr formBeanAttr;

        private static class FormBeanAttr {
            private PropertyDescriptor property;
            private Field field;
            private boolean validate;
            private Class<?>[] groups;

            private FormBeanAttr() {
            }

            private FormBeanAttr(PropertyDescriptor pd) {
                this.property = pd;
            }

            private FormBeanAttr(Field f) {
                this.field = f;
            }
        }

        private ActionEntryConfig(String name, Method method, ActionConfig container) {
            this(name, method, container, null, null);
        }

        private ActionEntryConfig(String name, Method method, ActionConfig container,
                Map<String, ActionResult> results, List<ActionException> exceptions) {
            this.name = name;
            this.method = method;
            this.container = container;
            this.results = results;
            this.exceptions = exceptions;

            analysisFormBeanAttr();
        }

        private void analysisFormBeanAttr() {
            FormBean formBean = method.getAnnotation(FormBean.class);

            if (formBean == null)
                formBean = container.acClass.getAnnotation(FormBean.class);

            if (formBean != null) {
                String value = formBean.value();

                if (GeneralHelper.isStrEmpty(value))
                    formBeanAttr = new FormBeanAttr();
                else {
                    Class<?> stopClass = ActionSupport.class.isAssignableFrom(container.acClass)
                            ? ActionSupport.class
                            : Object.class;
                    PropertyDescriptor pd = BeanHelper.getPropDescByName(container.acClass, stopClass, value);
                    Method setter = BeanHelper.getPropertyWriteMethod(pd);

                    if (setter != null)
                        formBeanAttr = new FormBeanAttr(pd);
                    else {
                        Field field = BeanHelper.getInstanceFiledByName(container.acClass, stopClass, value);

                        if (field != null)
                            formBeanAttr = new FormBeanAttr(field);
                    }

                    if (formBeanAttr == null) {
                        String msg = String.format(
                                "Parse @FormBean in '%s#%s()' -> no property or field named '%s'",
                                container.acClass.getName(), method.getName(), value);

                        throw new RuntimeException(msg);
                    }
                }

                formBeanAttr.validate = formBean.validate();
                formBeanAttr.groups = formBean.groups();
            }
        }
    }

    static class ActionPackage {
        private String path;
        private String name;

        private ActionPackage(String actionPath) {
            this(actionPath, null);
        }

        private ActionPackage(String actionPath, String currentPath) {
            int sepIndex = actionPath.lastIndexOf(PATH_SEPARATOR);

            if (sepIndex != -1) {
                path = actionPath.substring(0, sepIndex + 1);
                if (currentPath != null && path.startsWith(CURRENT_PATH_PREFIX))
                    path = path.replace(CURRENT_PATH_PREFIX, currentPath);
                if (!path.startsWith(PATH_SEPARATOR))
                    path = PATH_SEPARATOR + path;

                if (sepIndex < actionPath.length() - 1)
                    name = actionPath.substring(sepIndex + 1, actionPath.length());
            } else {
                path = PATH_SEPARATOR;
                name = actionPath;
            }
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();

            if (path != null)
                sb.append(path);
            if (name != null)
                sb.append(name);

            return sb.toString();
        }
    }

    private static class ActionFilterInfo {
        Class<ActionFilter> afClass;
        Pattern pattern;
        Pattern methods;
    }

    private static class ActionConfig {
        String name;
        Class<? extends Action> acClass;
        Map<String, ActionResult> results;
        List<ActionException> exceptions;
    }

    private static class ActionResult {
        String name;
        Action.ResultType type;
        String path;

        ActionResult() {

        }

        ActionResult(String name, ResultType type) {
            this(name, type, null);
        }

        ActionResult(String name, ResultType type, String path) {
            this.name = name;
            this.type = type;
            this.path = path;
        }
    }

    private static class ActionException {
        Class<? extends Exception> exception;
        String result;
    }

    /* **************************************************************************************************** */
    //                                 Action Convention                                 //
    /* **************************************************************************************************** */

    private void extractActionEntryConvention(ActionPackage pkg) throws Exception {
        ActionEntryDesc desc = ActionEntryDesc.generate(convention.basePackage, pkg);
        ActionEntryConfig aec = parseActionEntry(desc);

        tryPutActionEntryConfig(pkg, aec);

        return;
    }

    private void tryPutActionEntryConfig(ActionPackage pkg, ActionEntryConfig aec) {
        loadActionFilterCache(aec);

        synchronized (actionPkgMap) {
            Map<String, ActionEntryConfig> actionEntryMap = actionPkgMap.get(pkg.path);

            if (actionEntryMap == null) {
                actionEntryMap = new HashMap<String, ActionEntryConfig>();
                actionPkgMap.put(pkg.path, actionEntryMap);
            }

            GeneralHelper.syncTryPut(actionEntryMap, pkg.name, aec);
        }
    }

    private ActionEntryConfig parseActionEntry(ActionEntryDesc desc) {
        checkConvActionConfigMap(desc);
        ActionConfig ac = convACMap.get(desc.clazz);

        ActionEntryConfig aec = new ActionEntryConfig(desc.entryName, desc.method, ac);
        aec.results = parseResults(desc.method);
        aec.exceptions = parseExceptionLists(desc.method);

        return aec;
    }

    private void checkConvActionConfigMap(ActionEntryDesc desc) {
        if (!convACMap.containsKey(desc.clazz)) {
            ActionConfig ac = new ActionConfig();
            ac.name = desc.actionName;
            ac.acClass = desc.clazz;
            ac.results = parseResults(desc.clazz);
            ac.exceptions = parseExceptionLists(desc.clazz);

            GeneralHelper.syncTryPut(convACMap, ac.acClass, ac);
        }
    }

    private Map<String, ActionResult> parseResults(AnnotatedElement ae) {
        Map<String, Result> rsMap = parseResultAnnotations(ae);
        Map<String, ActionResult> arsMap = parseActionResultMap(rsMap);

        return arsMap.isEmpty() ? null : arsMap;
    }

    private Map<String, Result> parseResultAnnotations(AnnotatedElement ae) {
        Map<String, Result> map = new HashMap<String, Result>();
        Result result = ae.getAnnotation(Result.class);

        if (result != null)
            GeneralHelper.tryPut(map, result.value(), result);

        Results results = ae.getAnnotation(Results.class);

        if (results != null) {
            Result[] rs = results.value();
            for (Result r : rs)
                GeneralHelper.tryPut(map, r.value(), r);
        }

        return map;
    }

    private Map<String, ActionResult> parseActionResultMap(Map<String, Result> rsMap) {
        Map<String, ActionResult> map = new HashMap<String, ActionResult>();
        Collection<Result> results = rsMap.values();

        for (Result result : results) {
            ActionResult ars = new ActionResult();

            ars.name = result.value();
            ars.type = result.type();
            ars.path = parseResultPath(result.path());

            if (ars.type == Action.ResultType.DEFAULT) {
                if (!ars.name.equals(Action.NONE))
                    ars.type = Action.ResultType.DISPATCH;
                else
                    ars.type = Action.ResultType.FINISH;
            }

            map.put(ars.name, ars);
        }

        return map;
    }

    private List<ActionException> parseExceptionLists(AnnotatedElement ae) {
        List<ExceptionMapping> emList = parseExceptionMappingAnnotations(ae);
        List<ActionException> aeList = parseActionExceptionList(emList);

        return aeList.isEmpty() ? null : aeList;
    }

    private List<ExceptionMapping> parseExceptionMappingAnnotations(AnnotatedElement ae) {
        List<ExceptionMapping> list = new ArrayList<ExceptionMapping>();
        ExceptionMapping em = ae.getAnnotation(ExceptionMapping.class);

        if (em != null)
            list.add(em);

        ExceptionMappings ems = ae.getAnnotation(ExceptionMappings.class);

        if (ems != null) {
            ExceptionMapping[] emArr = ems.value();
            for (ExceptionMapping em2 : emArr)
                list.add(em2);
        }

        return list;
    }

    private List<ActionException> parseActionExceptionList(List<ExceptionMapping> emList) {
        List<ActionException> exceptions = new ArrayList<ActionException>();

        for (ExceptionMapping em : emList) {
            ActionException ae = new ActionException();

            ae.exception = em.value();
            ae.result = em.result();

            exceptions.add(ae);
        }

        return exceptions;
    }

    private static class BasePathConfig {
        Action.BaseType baseType = Action.BaseType.AUTO;
        String baseHref;
    }

    private static class I18nConfig {
        Locale defaultLocale;
        String defaultBundle = Action.Constant.DEFAULT_APP_BUNDLE;
    }

    static class BeanValidation {
        boolean enable;
        BeanValidator validator;
        String bundle = Action.Constant.DEFAULT_VLD_BUNDLE;
    }

    private static class ActionConvention {
        boolean enable;
        boolean detect;
        String basePackage;
        String dispatchPath;
        String physicalPath;
        String fileType;
        String separator;
    }

    private static class ActionEntryDesc {
        String actionPath;
        String actionName;
        String entryName;
        String className;
        String methodName;

        Class<? extends Action> clazz;
        Method method;

        static ActionEntryDesc generate(String basePackage, ActionPackage pkg)
                throws SecurityException, NoSuchMethodException {
            ActionEntryDesc desc = new ActionEntryDesc();
            desc.parseBasicInfo(basePackage, pkg);
            desc.parseEntryInfo();

            return desc;
        }

        private void parseBasicInfo(String basePackage, ActionPackage pkg) {
            this.actionPath = pkg.path;
            this.entryName = pkg.name;

            StringBuilder path = new StringBuilder(basePackage);
            String subPath = pkg.toString();

            if (!subPath.startsWith(PATH_SEPARATOR))
                path.append(PATH_SEPARATOR);

            path.append(subPath);

            String[] paths = GeneralHelper.splitStr(path.toString(), PATH_SEPARATOR + SUFFIX_CHARACTER);
            int index = paths.length - 1;
            String[] entry = GeneralHelper.splitStr(this.entryName, ACTION_ENTRY_SEPARATOR);
            this.actionName = entry[0];

            if (entry.length == 1)
                this.methodName = ACTION_DEFAULT_ENTRY_METHOD;
            else {
                paths[index] = entry[0];
                String[] halfNames = GeneralHelper.splitStr(entry[1], CONV_ACTION_PATH_NAME_SEPARATOR);
                StringBuilder name = new StringBuilder();

                for (int j = 0; j < halfNames.length; j++) {
                    String part = halfNames[j];

                    if (j == 0)
                        name.append(Character.toLowerCase(part.charAt(0)));
                    else
                        name.append(Character.toUpperCase(part.charAt(0)));

                    name.append(part.substring(1));
                }

                this.methodName = name.toString();
            }

            StringBuilder name = new StringBuilder();

            for (int i = 0; i <= index; i++) {
                String str = paths[i];
                String[] halfNames = GeneralHelper.splitStr(str, CONV_ACTION_PATH_NAME_SEPARATOR);

                for (int j = 0; j < halfNames.length; j++) {
                    String part = halfNames[j];

                    if (i < index)
                        name.append(part.toLowerCase());
                    else {
                        name.append(Character.toUpperCase(part.charAt(0)));
                        name.append(part.substring(1));
                    }
                }

                if (i < index)
                    name.append(SUFFIX_CHARACTER);
            }

            this.className = name.toString();
        }

        @SuppressWarnings("unchecked")
        private void parseEntryInfo() throws SecurityException, NoSuchMethodException {
            Class<?> clazz = GeneralHelper.classForName(className);
            if (clazz == null && !className.endsWith(CONV_ACTION_NAME_USUAL_ENDING))
                clazz = GeneralHelper.classForName(className + CONV_ACTION_NAME_USUAL_ENDING);

            if (clazz != null && Action.class.isAssignableFrom(clazz)
                    && BeanHelper.isPublicNotAbstractClass(clazz)) {
                Method method = clazz.getMethod(methodName);
                if (!String.class.isAssignableFrom(method.getReturnType())
                        || !BeanHelper.isPublicInstanceMethod(method)) {
                    String msg = String.format("invalid action entry method '%s' for '%s%s'", method, actionPath,
                            entryName);
                    throw new RuntimeException(msg);
                }

                this.clazz = (Class<? extends Action>) clazz;
                this.method = method;
            } else {
                String msg = String.format("invalid action class '%s[%s]' for '%s%s'", className,
                        CONV_ACTION_NAME_USUAL_ENDING, actionPath, entryName);
                throw new RuntimeException(msg);
            }
        }
    }

    private ActionResult makeConventionResult(String result, HttpServletRequest request, ActionPackage pkg,
            ActionEntryConfig aec) {
        final String SUCCESS_ENDS = convention.separator + Action.SUCCESS;

        String pkgNamePart = pkg.name.replace(ACTION_ENTRY_SEPARATOR, convention.separator);
        String fileNamePart = pkgNamePart + convention.separator + result;
        String fileName = fileNamePart + SUFFIX_CHARACTER + convention.fileType;
        String dispatchPath = null;

        if (!fileNamePart.endsWith(SUCCESS_ENDS))
            dispatchPath = makeDispatchPath(fileName, request, pkg, aec, convention.detect);
        else {
            dispatchPath = makeDispatchPath(fileName, request, pkg, aec, true);

            if (dispatchPath == null) {
                String fileNamePart2 = fileNamePart.substring(0, fileNamePart.length() - SUCCESS_ENDS.length());
                String fileName2 = fileNamePart2 + SUFFIX_CHARACTER + convention.fileType;
                dispatchPath = makeDispatchPath(fileName2, request, pkg, aec, true);

                if (dispatchPath == null && !convention.detect)
                    dispatchPath = makeDispatchPath(fileName, request, pkg, aec, false);
            }
        }

        if (dispatchPath != null) {
            tryPutActionResult(result, aec, dispatchPath);
            return aec.results.get(result);
        } else {
            String pkgPath = pkg.path.substring(1);
            String physicalPath = convention.physicalPath + pkgPath + fileName;
            String msg = String.format("physical target file '%s' not found for Action Result '%s' (%s)",
                    physicalPath, result, pkg);
            throw new RuntimeException(msg);
        }
    }

    private String makeDispatchPath(String fileName, HttpServletRequest request, ActionPackage pkg,
            ActionEntryConfig aec, boolean detect) {
        String pkgPath = pkg.path.substring(1);
        String dispatchPath = convention.dispatchPath + pkgPath + fileName;

        if (detect) {
            String physicalPath = convention.physicalPath + pkgPath + fileName;
            String absolutePath = HttpHelper.getRequestRealPath(request, physicalPath);

            if (!(new File(absolutePath).isFile()))
                return null;
        }

        return dispatchPath;
    }

    private void tryPutActionResult(String result, ActionEntryConfig aec, String dispatchPath) {
        synchronized (aec) {
            if (aec.results == null)
                aec.results = new HashMap<String, ActionResult>();

            GeneralHelper.syncTryPut(aec.results, result,
                    new ActionResult(result, Action.ResultType.DISPATCH, dispatchPath));
        }
    }

    /* **************************************************************************************************** */
    //                              Reload MVC Config                                    //
    /* **************************************************************************************************** */

    /** ?? {@linkplain ActionDispatcher}  */
    public static final ActionDispatcher instance() {
        return instance;
    }

    private void attachInstance() {
        if (instance != null)
            throw new IllegalStateException(String.format(
                    "another filter instance exists which is assignable to '%s' "
                            + "(only one such instance can be created for every application)",
                    ActionDispatcher.class.getName()));
        instance = this;

        /* ? HttpHelper  Servlet Context */
        HttpHelper.initializeServletContext(context);
    }

    private void detachInstance() {
        if (instance == this) {
            /*  HttpHelper  Servlet Context */
            HttpHelper.unInitializeServletContext();

            instance = null;
        }
    }

    /** ? HTTP  {@linkplain ActionDispatcher#reload(long) reload(long)} ?<br>
     * 
     * ?? HTTP 503 
     * 
     */
    public void pause() {
        pausing = true;
    }

    /** ?? HTTP  {@linkplain ActionDispatcher#reload(long) reload(long)} ? */
    public void resume() {
        pausing = false;
    }

    /** ? MVC ?<br>
     * 
     * @param delay         : ?????????
     * @throws Exception   : ?????
     * 
     */
    synchronized public void reload(long delay) throws Exception {
        String encoding = this.encoding;
        String actionSuffix = this.actionSuffix;
        BasePathConfig basePath = this.basePath;
        I18nConfig i18nCfg = this.i18nCfg;
        BeanValidation validation = this.validation;
        ActionConvention convention = this.convention;
        Map<String, ActionResult> globalResults = this.globalResults;
        List<ActionException> globalExceptions = this.globalExceptions;
        List<ActionFilterInfo> filterInfoList = this.filterInfoList;
        LinkedList<ActionFilter> filterList = this.filterList;
        Map<String, Map<String, ActionEntryConfig>> actionPkgMap = this.actionPkgMap;
        Map<Method, LinkedList<ActionFilter>> filterCache = this.filterCache;
        Map<Class<ActionFilter>, ActionFilter> filterMap = this.filterMap;
        Map<Class<? extends Action>, ActionConfig> convACMap = this.convACMap;
        Map<String, String> resultAliasMap = this.resultAliasMap;

        try {
            GeneralHelper.waitFor(delay);

            for (ActionFilter filter : filterList)
                filter.destroy();

            if (validation.validator != null)
                validation.validator.destroy();

            loadConfig();
        } catch (Exception e) {
            for (ActionFilter filter : this.filterList)
                filter.destroy();

            if (this.validation.validator != null)
                this.validation.validator.destroy();

            if (validation.validator != null)
                validation.validator.init();

            for (ActionFilter filter : filterList)
                filter.init();

            this.encoding = encoding;
            this.actionSuffix = actionSuffix;
            this.basePath = basePath;
            this.i18nCfg = i18nCfg;
            this.validation = validation;
            this.convention = convention;
            this.globalResults = globalResults;
            this.globalExceptions = globalExceptions;
            this.filterInfoList = filterInfoList;
            this.filterList = filterList;
            this.actionPkgMap = actionPkgMap;
            this.filterCache = filterCache;
            this.filterMap = filterMap;
            this.convACMap = convACMap;
            this.resultAliasMap = resultAliasMap;

            throw e;
        }
    }
}