com.sinosoft.one.mvc.web.impl.thread.ActionEngine.java Source code

Java tutorial

Introduction

Here is the source code for com.sinosoft.one.mvc.web.impl.thread.ActionEngine.java

Source

/*
 * Copyright 2007-2009 the original author or authors.
 *
 * 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 com.sinosoft.one.mvc.web.impl.thread;

import static org.springframework.validation.BindingResult.MODEL_KEY_PREFIX;

import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

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

import com.sinosoft.one.mvc.MvcVersion;
import com.sinosoft.one.mvc.adapter.ArraysEx;
import com.sinosoft.one.mvc.util.MvcStringUtil;
import com.sinosoft.one.mvc.web.ControllerInterceptor;
import com.sinosoft.one.mvc.web.InterceptorDelegate;
import com.sinosoft.one.mvc.web.Invocation;
import com.sinosoft.one.mvc.web.InvocationChain;
import com.sinosoft.one.mvc.web.ParamValidator;
import com.sinosoft.one.mvc.web.RequestPath;
import com.sinosoft.one.mvc.web.annotation.HttpFeatures;
import com.sinosoft.one.mvc.web.annotation.IfParamExists;
import com.sinosoft.one.mvc.web.annotation.Intercepted;
import com.sinosoft.one.mvc.web.annotation.Return;
import com.sinosoft.one.mvc.web.impl.module.Module;
import com.sinosoft.one.mvc.web.impl.validation.ParameterBindingResult;
import com.sinosoft.one.mvc.web.paramresolver.MethodParameterResolver;
import com.sinosoft.one.mvc.web.paramresolver.ParamMetaData;
import com.sinosoft.one.mvc.web.paramresolver.ParamResolver;
import com.sinosoft.one.mvc.web.paramresolver.ParameterNameDiscovererImpl;
import com.sinosoft.one.mvc.web.paramresolver.ResolverFactoryImpl;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.SpringVersion;
import org.springframework.util.Assert;
import org.springframework.validation.Errors;

/**
 *
 */
public final class ActionEngine implements Engine {

    private final static Log logger = LogFactory.getLog(ActionEngine.class);

    private final Module module;

    private final Class<?> controllerClass;

    private final Object controller;

    private final Method method;

    private final HttpFeatures httpFeatures;

    private final InterceptorDelegate[] interceptors;

    //private final ParamValidator[] validators;
    /*
     * 2012-7-5
     * 
     * ?validators[]?,
     * ??paramvalidator
     */
    private final ParamValidator[][] validators;

    private final ParamExistenceChecker[] paramExistenceChecker;

    private final MethodParameterResolver methodParameterResolver;

    private transient String toStringCache;

    public ActionEngine(Module module, Class<?> controllerClass, Object controller, Method method) {
        this.module = module;
        this.controllerClass = controllerClass;
        this.controller = controller;
        this.method = method;
        this.interceptors = compileInterceptors();
        this.methodParameterResolver = compileParamResolvers();
        //this.validators = compileValidators();
        this.validators = compileMultipleValidators();
        this.paramExistenceChecker = compileParamExistenceChecker();
        HttpFeatures httpFeatures = method.getAnnotation(HttpFeatures.class);
        if (httpFeatures == null) {
            httpFeatures = this.controllerClass.getAnnotation(HttpFeatures.class);
        }
        this.httpFeatures = httpFeatures;
    }

    public InterceptorDelegate[] getRegisteredInterceptors() {
        return interceptors;
    }

    public Class<?> getControllerClass() {
        return controllerClass;
    }

    public Object getController() {
        return controller;
    }

    public Method getMethod() {
        return method;
    }

    public String[] getParameterNames() {
        return methodParameterResolver.getParameterNames();
    }

    private MethodParameterResolver compileParamResolvers() {
        ParameterNameDiscovererImpl parameterNameDiscoverer = new ParameterNameDiscovererImpl();
        ResolverFactoryImpl resolverFactory = new ResolverFactoryImpl();
        for (ParamResolver resolver : module.getCustomerResolvers()) {
            resolverFactory.addCustomerResolver(resolver);
        }
        return new MethodParameterResolver(this.controllerClass, method, parameterNameDiscoverer, resolverFactory);
    }

    /**
     * ?paramvalidator
     * @return
     */
    @SuppressWarnings("rawtypes")
    private ParamValidator[][] compileMultipleValidators() {
        Class[] parameterTypes = method.getParameterTypes();
        List<ParamValidator> validators = module.getValidators();
        ParamValidator[][] registeredValidators = new ParamValidator[parameterTypes.length][0];
        for (int i = 0; i < parameterTypes.length; i++) {
            int multipleIndex = 0;
            for (ParamValidator validator : validators) {
                if (validator.supports(methodParameterResolver.getParamMetaDatas()[i])) {
                    registeredValidators[i] = ArraysEx.copyOf(registeredValidators[i], multipleIndex + 1);
                    registeredValidators[i][multipleIndex++] = validator;
                }
            }
        }

        return registeredValidators;
    }

    private InterceptorDelegate[] compileInterceptors() {
        List<InterceptorDelegate> interceptors = module.getInterceptors();
        List<InterceptorDelegate> registeredInterceptors = new ArrayList<InterceptorDelegate>(interceptors.size());
        for (InterceptorDelegate interceptor : interceptors) {

            ControllerInterceptor most = InterceptorDelegate.getMostInnerInterceptor(interceptor);

            if (!most.getClass().getName().startsWith("com.sinosoft.one.mvc.web")) {

                // ?@Intercepted (@Intercepted??????)
                Intercepted intercepted = method.getAnnotation(Intercepted.class);
                if (intercepted == null) {
                    // @Inheritedannotationclass.getAnnotation????
                    intercepted = this.controllerClass.getAnnotation(Intercepted.class);
                }
                // @Interceptedallowdeny
                if (intercepted != null) {
                    // 3.1 deny?
                    if (MvcStringUtil.matches(intercepted.deny(), interceptor.getName())) {
                        if (logger.isDebugEnabled()) {
                            logger.debug("action '" + controllerClass.getName() + "#" + method.getName()
                                    + "': remove interceptor by @Intercepted.deny: " + most.getClass().getName());
                        }
                        continue;
                    }
                    // 3.2 allow?
                    if (!MvcStringUtil.matches(intercepted.allow(), interceptor.getName())) {
                        if (logger.isDebugEnabled()) {
                            logger.debug("action '" + controllerClass.getName() + "#" + method.getName()
                                    + "': remove interceptor by @Intercepted.allow: " + most.getClass().getName());
                        }
                        continue;
                    }
                }
            }
            // ????
            if (interceptor.isForAction(controllerClass, method)) {
                registeredInterceptors.add(interceptor);
            } else {
                if (logger.isDebugEnabled()) {
                    logger.debug("action '" + controllerClass.getName() + "#" + method.getName()
                            + "': remove interceptor by interceptor.isForAction: " + most.getClass().getName());
                }
            }
        }
        //

        if (logger.isDebugEnabled()) {
            logger.debug("interceptors of " + controllerClass.getName() + "#" + method.getName() + "=("
                    + registeredInterceptors.size() + "/" + interceptors.size() + ")" + registeredInterceptors);
        }
        return registeredInterceptors.toArray(new InterceptorDelegate[registeredInterceptors.size()]);

    }

    /**
     * ?{@link IfParamExists}
     * 
     * @author Li Weibo (weibo.leo@gmail.com)
     */
    private static interface ParamExistenceChecker {

        public int check(Map<String, String[]> params);
    }

    /**
     * ???{@link ParamExistenceChecker}
     * 
     * @return
     */
    private ParamExistenceChecker[] compileParamExistenceChecker() {

        IfParamExists ifParamExists = method.getAnnotation(IfParamExists.class);
        //IfParamExistsIfParamExists("")?
        if (ifParamExists == null || ifParamExists.value().trim().length() == 0) {
            return new ParamExistenceChecker[] {};
        }

        List<ParamExistenceChecker> checkers = new ArrayList<ParamExistenceChecker>(); //?
        String value = ifParamExists.value();

        //???: type&subtype=value&anothername=value2
        String[] terms = StringUtils.split(value, "&");
        Assert.isTrue(terms.length >= 1); //?

        //'&'??term?
        for (final String term : terms) {
            final int index = term.indexOf('='); //'='
            if (index == -1) { //=????term???
                checkers.add(new ParamExistenceChecker() {

                    final String paramName = term.trim();

                    public int check(Map<String, String[]> params) {
                        String[] paramValues = params.get(paramName);
                        if (logger.isDebugEnabled()) {
                            logger.debug(this.toString() + " is checking param:" + paramName + "="
                                    + Arrays.toString(paramValues));
                        }

                        //????ok
                        if (paramValues != null && paramValues.length > 0) {
                            return 10;
                        } else {
                            return -1;
                        }
                    }
                });
            } else { //term'='

                final String paramName = term.substring(0, index).trim(); //???
                final String expected = term.substring(index + 1).trim(); //?

                if (expected.startsWith(":")) { //expected?
                    Pattern tmpPattern = null;
                    try {
                        tmpPattern = Pattern.compile(expected.substring(1));
                    } catch (PatternSyntaxException e) {
                        logger.error("@IfParamExists pattern error, " + controllerClass.getName() + "#"
                                + method.getName(), e);
                    }
                    final Pattern pattern = tmpPattern; //?final
                    checkers.add(new ParamExistenceChecker() {

                        public int check(Map<String, String[]> params) {
                            String[] paramValues = params.get(paramName);
                            if (logger.isDebugEnabled()) {
                                logger.debug(this.toString() + " is checking param:" + paramName + "="
                                        + Arrays.toString(paramValues) + ", pattern=" + pattern.pattern());
                            }
                            if (paramValues == null) { //???
                                return -1;
                            }

                            for (String paramValue : paramValues) {
                                if (pattern != null && pattern.matcher(paramValue).matches()) {
                                    return 12;
                                }
                            }
                            return -1;
                        }
                    });
                } else { //expected?""
                    checkers.add(new ParamExistenceChecker() {

                        public int check(Map<String, String[]> params) {
                            String[] paramValues = params.get(paramName);
                            if (logger.isDebugEnabled()) {
                                logger.debug(this.toString() + " is checking param:" + paramName + "="
                                        + Arrays.toString(paramValues) + ", expected=" + expected);
                            }
                            if (paramValues == null) { //???
                                return -1;
                            }

                            for (String paramValue : paramValues) {
                                if (expected.equals(paramValue)) {
                                    return 13;// 13?12
                                }
                            }
                            return -1;
                        }
                    });
                }
            }
        }
        return checkers.toArray(new ParamExistenceChecker[] {});
    }

    public int isAccepted(HttpServletRequest request) {
        if (paramExistenceChecker.length == 0) { //??1
            return 1;
        }
        int total = 0;
        Map<String, String[]> params = resolveQueryString(request.getQueryString());
        for (ParamExistenceChecker checker : paramExistenceChecker) {
            int c = checker.check(params);
            if (c == -1) { //-1??
                if (logger.isDebugEnabled()) {
                    logger.debug("Accepted check not passed by " + checker.toString());
                }
                return -1;
            }
            //FIXME ??????
            //?????
            total += c;
        }
        return total;
    }

    private Map<String, String[]> resolveQueryString(String queryString) {
        Map<String, String[]> params;
        if (queryString == null || queryString.length() == 0) {
            params = Collections.emptyMap();
        } else {
            params = new HashMap<String, String[]>();
            String[] kvs = queryString.split("&");
            for (String kv : kvs) {
                String[] pair = kv.split("=");
                if (pair.length == 2) {
                    mapPut(params, pair[0], pair[1]);
                } else if (pair.length == 1) {
                    mapPut(params, pair[0], "");
                } else {
                    logger.error("Illegal queryString:" + queryString);
                }
            }
        }
        return params;
    }

    private void mapPut(Map<String, String[]> map, String key, String value) {
        String[] values = map.get(key);
        if (values == null) {
            values = new String[] { value };
        } else {
            values = ArraysEx.copyOf(values, values.length + 1);
            values[values.length - 1] = value;
        }
        map.put(key, values);
    }

    public Object execute(Mvc mvc) throws Throwable {
        try {
            return innerExecute(mvc);
        } catch (Throwable local) {
            throw createException(mvc, local);
        }
    }

    protected Object innerExecute(Mvc mvc) throws Throwable {
        Invocation inv = mvc.getInvocation();

        // creates parameter binding result (not bean, just simple type, like int, Integer, int[] ...
        ParameterBindingResult paramBindingResult = new ParameterBindingResult(inv);
        String paramBindingResultName = MODEL_KEY_PREFIX + paramBindingResult.getObjectName();
        inv.addModel(paramBindingResultName, paramBindingResult);

        // resolves method parameters, adds the method parameters to model
        Object[] methodParameters = methodParameterResolver.resolve(inv, paramBindingResult);
        ((InvocationBean) inv).setMethodParameters(methodParameters);
        String[] parameterNames = methodParameterResolver.getParameterNames();

        Object instruction = null;

        ParamMetaData[] metaDatas = methodParameterResolver.getParamMetaDatas();
        // validators
        /*
         for (int i = 0; i < this.validators.length; i++) {
        if (validators[i] != null && !(methodParameters[i] instanceof Errors)) {
            Errors errors = inv.getBindingResult(parameterNames[i]);
            instruction = validators[i].validate(//
                    metaDatas[i], inv, methodParameters[i], errors);
            if (logger.isDebugEnabled()) {
                logger.debug("do validate [" + validators[i].getClass().getName()
                        + "] and return '" + instruction + "'");
            }
            // instruction?null?boolean==>???
            if (instruction != null) {
                if (instruction instanceof Boolean) {
                    continue;
                }
                if (instruction instanceof String && ((String) instruction).length() == 0) {
                    continue;
                }
                return instruction;
            }
        }
        }
         */
        /*
         * parametervalidator?
         */
        for (int i = 0; i < this.validators.length; i++) {
            for (int j = 0; j < this.validators[i].length; j++) {
                if (validators[i][j] != null && !(methodParameters[i] instanceof Errors)) {
                    Errors errors = inv.getBindingResult(parameterNames[i]);
                    instruction = validators[i][j].validate(//
                            metaDatas[i], inv, methodParameters[i], errors);
                    if (logger.isDebugEnabled()) {
                        logger.debug("do validate [" + validators[i].getClass().getName() + "] and return '"
                                + instruction + "'");
                    }
                    // instruction?null?boolean==>???
                    if (instruction != null) {
                        if (instruction instanceof Boolean) {
                            continue;
                        }
                        if (instruction instanceof String && ((String) instruction).length() == 0) {
                            continue;
                        }
                        return instruction;
                    }
                }
            }
        }

        //
        for (int i = 0; i < parameterNames.length; i++) {
            if (parameterNames[i] != null && methodParameters[i] != null
                    && inv.getModel().get(parameterNames[i]) != methodParameters[i]) {
                inv.addModel(parameterNames[i], methodParameters[i]);
            }
        }

        // intetceptors & controller
        return new InvocationChainImpl(mvc).doNext();
    }

    private class InvocationChainImpl implements InvocationChain {

        private final boolean debugEnabled = logger.isDebugEnabled();

        private int index = -1;

        private final Mvc mvc;

        private Object instruction;

        public InvocationChainImpl(Mvc mvc) {
            this.mvc = mvc;
        }

        public Object doNext() throws Exception {
            if (++index < interceptors.length) { // ++index -10
                InterceptorDelegate interceptor = interceptors[index];
                //
                mvc.addAfterCompletion(interceptor);
                Object instruction = interceptor.roundInvocation(mvc.getInvocation(), this);
                //
                if (debugEnabled) {
                    logger.debug(
                            "interceptor[" + interceptor.getName() + "] do round and return '" + instruction + "'");
                }

                // null???instruction
                // ?!!
                if (instruction != null) {
                    this.instruction = instruction;
                }
                return this.instruction;
            } else if (index == interceptors.length) {
                // applies http features before the resolvers
                if (httpFeatures != null) {
                    applyHttpFeatures(mvc.getInvocation());
                }

                this.instruction = method.invoke(controller, mvc.getInvocation().getMethodParameters());

                // @Return
                if (this.instruction == null) {
                    Return returnAnnotation = method.getAnnotation(Return.class);
                    if (returnAnnotation != null) {
                        this.instruction = returnAnnotation.value();
                    }
                }
                return this.instruction;
            }
            throw new IndexOutOfBoundsException("don't call twice 'chain.doNext()' in one intercpetor; index="
                    + index + "; interceptors.length=" + interceptors.length);
        }

    }

    private Exception createException(Mvc mvc, Throwable exception) {
        final RequestPath requestPath = mvc.getInvocation().getRequestPath();
        StringBuilder sb = new StringBuilder(1024);
        sb.append("[Mvc-").append(MvcVersion.getVersion()).append("@Spring-").append(SpringVersion.getVersion());
        sb.append("]Error happended: ").append(requestPath.getMethod());
        sb.append(" ").append(requestPath.getUri());
        sb.append("->");
        sb.append(this).append(" params=");
        sb.append(Arrays.toString(mvc.getInvocation().getMethodParameters()));
        InvocationTargetException servletException = new InvocationTargetException(exception, sb.toString());
        return servletException;
    }

    private void applyHttpFeatures(final Invocation inv) throws UnsupportedEncodingException {
        HttpServletResponse response = inv.getResponse();
        if (StringUtils.isNotBlank(httpFeatures.charset())) {
            response.setCharacterEncoding(httpFeatures.charset());
            if (logger.isDebugEnabled()) {
                logger.debug("set response.characterEncoding by HttpFeatures:" + httpFeatures.charset());
            }
        }
        if (StringUtils.isNotBlank(httpFeatures.contentType())) {
            String contentType = httpFeatures.contentType();
            if (contentType.equals("json")) {
                contentType = "application/json";
            } else if (contentType.equals("xml")) {
                contentType = "text/xml";
            } else if (contentType.equals("html")) {
                contentType = "text/html";
            } else if (contentType.equals("plain") || contentType.equals("text")) {
                contentType = "text/plain";
            }
            response.setContentType(contentType);
            if (logger.isDebugEnabled()) {
                logger.debug("set response.contentType by HttpFeatures:" + response.getContentType());
            }
        }
    }

    @Override
    public String toString() {
        if (toStringCache == null) {
            Class<?>[] parameterTypes = method.getParameterTypes();
            String appPackageName = this.controllerClass.getPackage().getName();
            if (appPackageName.indexOf('.') != -1) {
                appPackageName = appPackageName.substring(0, appPackageName.lastIndexOf('.'));
            }
            String methodParamNames = "";
            for (int i = 0; i < parameterTypes.length; i++) {
                if (methodParamNames.length() == 0) {
                    methodParamNames = showSimpleName(parameterTypes[i], appPackageName);
                } else {
                    methodParamNames = methodParamNames + ", " + showSimpleName(parameterTypes[i], appPackageName);
                }
            }
            toStringCache = ""//
                    + showSimpleName(method.getReturnType(), appPackageName) + " " + method.getName() //
                    + "(" + methodParamNames + ")";
        }
        return toStringCache;
    }

    private String showSimpleName(Class<?> parameterType, String appPackageName) {
        if (parameterType.getName().startsWith("net.paoding") || parameterType.getName().startsWith("java.lang")
                || parameterType.getName().startsWith("java.util")
                || parameterType.getName().startsWith(appPackageName)) {
            return parameterType.getSimpleName();
        }
        return parameterType.getName();
    }

    public void destroy() {

    }
}