org.jessma.ext.rest.RestDispatcher.java Source code

Java tutorial

Introduction

Here is the source code for org.jessma.ext.rest.RestDispatcher.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.ext.rest;

import static org.jessma.ext.rest.RequestType.GET;
import static org.jessma.ext.rest.RequestType.POST;
import static org.jessma.ext.rest.Rest.Constant.REQ_ATTR_REST_CONTEXT;
import static org.jessma.ext.rest.Rest.Constant.REQ_PARAM_REST_METHOD;
import static org.jessma.ext.rest.Rest.Constant.REQ_PARAM_REST_RENDER;
import static org.jessma.ext.rest.renderer.RenderType.HTML;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
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.ext.rest.renderer.RenderType;
import org.jessma.util.CryptHelper;
import org.jessma.util.GeneralHelper;
import org.jessma.util.http.HttpHelper;

/**
* 
* REST ? {@link Filter}
*
*/
public class RestDispatcher implements Filter {
    private static final String PATH_SEPARATOR = HttpHelper.URL_PATH_SEPARATOR;
    private static final String SUFFIX_CHARACTER = ".";

    private static final RenderType DEFAULT_RENDER_TYPE = HTML;
    private static final String DEFAULT_ACTION_SUFFIX = ".action";
    private static final String DEFAULT_REST_BASE_PATH = PATH_SEPARATOR;
    private static final String DEFAULT_DEF_ACTION_PATH = PATH_SEPARATOR;
    private static final String DEFAULT_SUPPORT_RENDER_TYPES = DEFAULT_RENDER_TYPE.toString();

    private static final String GLOBAL_KEY = "global";
    private static final String ENCODING_KEY = "encoding";
    private static final String ACTION_SUFFIX_KEY = "action-suffix";
    private static final String REST_BASE_PATH_KEY = "rest-base-path";
    private static final String DEFAULT_ACTION_PATH_KEY = "default-action-path";
    private static final String SUPPORT_RENDER_TYPES_KEY = "support-render-types";
    private static final String ENTITIS_KEY = "entities";
    private static final String ENTITIS_ACTION_PATH_KEY = "action-path";
    private static final String ENTITY_KEY = "entity";
    private static final String ENTITY_NAME_KEY = "name";
    private static final String ENTITY_ACTION_KEY = "action";

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

    private static final String REQ_TYPE_GET = GET.toString();
    private static final String REQ_TYPE_POST = POST.toString();

    private static final Map<String, RequestType> SUPPORTED_REQ_TYPES_MAP = new HashMap<String, RequestType>();
    private static final Map<String, RenderType> SUPPORTED_RENDER_TYPES_MAP = new HashMap<String, RenderType>();

    static {
        for (RequestType type : RequestType.values())
            SUPPORTED_REQ_TYPES_MAP.put(type.toString(), type);

        for (RenderType type : RenderType.values())
            SUPPORTED_RENDER_TYPES_MAP.put(type.toString(), type);
    }

    private static RestDispatcher instance;

    private FilterConfig filterCfg;
    private int contextPathLength;
    private boolean pausing;

    private String encoding;
    private String actionSuffix;
    private String restBasePath;
    private String defaultActionPath;
    private RenderType defaultRenderType;

    private Map<String, RenderType> supportRenderTypes = new HashMap<String, RenderType>();
    private Map<String, ActionInfo> entityMap = new HashMap<String, RestDispatcher.ActionInfo>();

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

        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(RestDispatcher.class, confFile);

        resetProperties();
        loadConfigFile(confFile);
    }

    private void resetProperties() {
        encoding = null;
        actionSuffix = DEFAULT_ACTION_SUFFIX;
        restBasePath = DEFAULT_REST_BASE_PATH;
        defaultActionPath = DEFAULT_DEF_ACTION_PATH;
        defaultRenderType = DEFAULT_RENDER_TYPE;
        supportRenderTypes = new HashMap<String, RenderType>();
        entityMap = new HashMap<String, RestDispatcher.ActionInfo>();
    }

    private void loadConfigFile(String confFile) throws ServletException {
        try {
            SAXReader sr = new SAXReader();
            Document doc = sr.read(new File(confFile));
            Element root = doc.getRootElement();

            parseGlobal(root);
            parseEntities(root);
        } catch (Exception e) {
            throw new ServletException("load REST config fail", e);
        }
    }

    private void parseGlobal(Element root) {
        Element global = root.element(GLOBAL_KEY);
        if (global != null) {
            Element enc = global.element(ENCODING_KEY);
            if (enc != null) {
                String str = enc.getTextTrim();
                if (GeneralHelper.isStrNotEmpty(str))
                    encoding = str;
            }

            Element acSuffix = global.element(ACTION_SUFFIX_KEY);
            if (acSuffix != null) {
                String str = acSuffix.getTextTrim();
                if (GeneralHelper.isStrNotEmpty(str)) {
                    if (str.startsWith(SUFFIX_CHARACTER))
                        actionSuffix = str;
                    else
                        actionSuffix = SUFFIX_CHARACTER + str;
                }
            }

            Element rbPath = global.element(REST_BASE_PATH_KEY);
            if (rbPath != null) {
                String str = rbPath.getTextTrim();
                if (GeneralHelper.isStrNotEmpty(str))
                    restBasePath = HttpHelper.ensurePath(str, DEFAULT_REST_BASE_PATH);
            }

            Element defAcPath = global.element(DEFAULT_ACTION_PATH_KEY);
            if (defAcPath != null) {
                String str = defAcPath.getTextTrim();
                if (GeneralHelper.isStrNotEmpty(str))
                    defaultActionPath = HttpHelper.ensurePath(str, DEFAULT_DEF_ACTION_PATH);
            }
        }

        parseSupportRenderTypes(global);
    }

    private void parseSupportRenderTypes(Element global) {
        String types = DEFAULT_SUPPORT_RENDER_TYPES;

        if (global != null) {
            Element srTypes = global.element(SUPPORT_RENDER_TYPES_KEY);
            if (srTypes != null) {
                String str = srTypes.getTextTrim();
                if (GeneralHelper.isStrNotEmpty(str))
                    types = str;
            }
        }

        String[] ts = GeneralHelper.splitStr(types);

        for (int i = 0; i < ts.length; i++) {
            String type = ts[i];
            RenderType rt = SUPPORTED_RENDER_TYPES_MAP.get(type);

            if (rt == null) {
                String msg = String.format("parse '%s' fail, only support %s", SUPPORT_RENDER_TYPES_KEY,
                        SUPPORTED_RENDER_TYPES_MAP.keySet());
                throw new RestException(msg);
            }

            if (i == 0)
                defaultRenderType = rt;
            supportRenderTypes.put(type, rt);
        }
    }

    @SuppressWarnings("unchecked")
    private void parseEntities(Element root) {
        List<Element> entsList = root.elements(ENTITIS_KEY);
        for (Element entities : entsList) {
            String path = entities.attributeValue(ENTITIS_ACTION_PATH_KEY);
            path = HttpHelper.ensurePath(path, PATH_SEPARATOR);

            List<Element> entList = entities.elements(ENTITY_KEY);

            for (Element entity : entList) {
                String name = entity.attributeValue(ENTITY_NAME_KEY);

                if (GeneralHelper.isStrNotEmpty(name) && !entityMap.containsKey(name)) {
                    String action = entity.attributeValue(ENTITY_ACTION_KEY);

                    if (GeneralHelper.isStrEmpty(action))
                        action = name;

                    entityMap.put(name, new ActionInfo(path, action));
                }
            }
        }
    }

    @Override
    public void destroy() {
        entityMap = null;
        supportRenderTypes = 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.getRequestURI();
        reqPath = CryptHelper.urlDecode(reqPath, encoding);
        reqPath = reqPath.substring(contextPathLength);

        if (!process(request, response, reqPath))
            chain.doFilter(request, response);
    }

    private boolean process(HttpServletRequest request, HttpServletResponse response, String reqPath)
            throws ServletException, IOException {
        if (reqPath.startsWith(restBasePath)) {
            int begin = restBasePath.length();
            int index = parseEntityEndIndex(reqPath, begin);

            if (index > begin) {
                RestContext context = new RestContext();

                if (!parseRequestPathAndRenderType(request, reqPath, index, context))
                    return false;

                if (!parseRequestType(request, context))
                    return false;

                String actionPath = parseActionPath(reqPath, begin, index);

                request.setAttribute(REQ_ATTR_REST_CONTEXT, context);
                request.getRequestDispatcher(actionPath).forward(request, response);

                return true;
            }
        }

        return false;
    }

    private int parseEntityEndIndex(String reqPath, int begin) {
        int index = reqPath.indexOf(PATH_SEPARATOR, begin);

        if (index == -1) {
            index = reqPath.lastIndexOf(SUFFIX_CHARACTER);

            if (index == -1)
                index = reqPath.length();
        }

        return index;
    }

    private boolean parseRequestPathAndRenderType(HttpServletRequest request, String reqPath, int index,
            RestContext context) {
        String suffix = null;
        int pos1 = reqPath.lastIndexOf(SUFFIX_CHARACTER);

        if (pos1 == -1)
            context.requestPath = reqPath.substring(index);
        else {
            int pos2 = reqPath.lastIndexOf(PATH_SEPARATOR);

            if (pos1 < pos2)
                context.requestPath = reqPath.substring(index);
            else {
                context.requestPath = reqPath.substring(index, pos1);
                suffix = reqPath.substring(pos1 + 1);
            }
        }

        String render = suffix;
        String restRender = request.getParameter(REQ_PARAM_REST_RENDER);

        if (restRender != null) {
            render = restRender.toLowerCase();
            if (!supportRenderTypes.containsKey(restRender)) {
                String msg = String.format("request param '%s' contains illegal value '%s'", REQ_PARAM_REST_RENDER,
                        restRender);
                throw new RestException(msg);
            }
        }

        RenderType type = (render == null) ? defaultRenderType : supportRenderTypes.get(render);

        if (type != null) {
            context.renderType = type;
            return true;
        }

        return false;
    }

    private boolean parseRequestType(HttpServletRequest request, RestContext context) {
        String method = request.getMethod();

        if (method.equals(REQ_TYPE_GET) || method.equals(REQ_TYPE_POST)) {
            String restMethod = request.getParameter(REQ_PARAM_REST_METHOD);

            if (restMethod != null) {
                method = restMethod.toUpperCase();
                if (!SUPPORTED_REQ_TYPES_MAP.containsKey(method)) {
                    String msg = String.format("request param '%s' contains illegal value '%s'",
                            REQ_PARAM_REST_METHOD, restMethod);
                    throw new RestException(msg);
                }
            }
        }

        RequestType type = SUPPORTED_REQ_TYPES_MAP.get(method);

        if (type != null) {
            context.requestType = type;
            return true;
        }

        return false;
    }

    private String parseActionPath(String reqPath, int begin, int index) {
        String entity = reqPath.substring(begin, index);

        ActionInfo info = entityMap.get(entity);
        if (info == null)
            info = new ActionInfo(defaultActionPath, entity);

        return info + actionSuffix;
    }

    private static class ActionInfo {
        String path;
        String name;

        ActionInfo(String path, String name) {
            this.path = path;
            this.name = name;
        }

        @Override
        public String toString() {
            return HttpHelper.ensurePath(path, PATH_SEPARATOR) + name;
        }
    }

    /* **************************************************************************************************** */
    //                              Reload REST Config                                    //
    /* **************************************************************************************************** */

    /** ? REST  {@linkplain RestDispatcher}  */
    public static final RestDispatcher 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)",
                    RestDispatcher.class.getName()));
        instance = this;
    }

    private void detachInstance() {
        if (instance == this)
            instance = null;
    }

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

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

    /** ? REST ?<br>
     * 
     * @param delay         : ?????????
     * @throws Exception   : ?????
     * 
     */
    synchronized public void reload(long delay) throws Exception {
        String encoding = this.encoding;
        String actionSuffix = this.actionSuffix;
        String restBasePath = this.restBasePath;
        String defaultActionPath = this.defaultActionPath;
        RenderType defaultRenderType = this.defaultRenderType;

        Map<String, RenderType> supportRenderTypes = this.supportRenderTypes;
        Map<String, ActionInfo> entityMap = this.entityMap;

        try {
            GeneralHelper.waitFor(delay);
            loadConfig();
        } catch (Exception e) {
            this.encoding = encoding;
            this.actionSuffix = actionSuffix;
            this.restBasePath = restBasePath;
            this.defaultActionPath = defaultActionPath;
            this.defaultRenderType = defaultRenderType;
            this.supportRenderTypes = supportRenderTypes;
            this.entityMap = entityMap;

            throw e;
        }
    }
}