Java tutorial
/* * 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 net.paoding.rose.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 net.paoding.rose.RoseVersion; import net.paoding.rose.util.RoseStringUtil; import net.paoding.rose.web.ControllerInterceptor; import net.paoding.rose.web.InterceptorDelegate; import net.paoding.rose.web.Invocation; import net.paoding.rose.web.InvocationChain; import net.paoding.rose.web.ParamValidator; import net.paoding.rose.web.RequestPath; import net.paoding.rose.web.annotation.HttpFeatures; import net.paoding.rose.web.annotation.IfParamExists; import net.paoding.rose.web.annotation.Intercepted; import net.paoding.rose.web.annotation.Return; import net.paoding.rose.web.impl.module.Module; import net.paoding.rose.web.impl.validation.ParameterBindingResult; import net.paoding.rose.web.paramresolver.MethodParameterResolver; import net.paoding.rose.web.paramresolver.ParamMetaData; import net.paoding.rose.web.paramresolver.ParamResolver; import net.paoding.rose.web.paramresolver.ParameterNameDiscovererImpl; import net.paoding.rose.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; /** * @author [qieqie.wang@gmail.com] */ 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; 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.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); } @SuppressWarnings("unchecked") private ParamValidator[] compileValidators() { Class[] parameterTypes = method.getParameterTypes(); List<ParamValidator> validators = module.getValidators(); ParamValidator[] registeredValidators = new ParamValidator[parameterTypes.length]; for (int i = 0; i < parameterTypes.length; i++) { for (ParamValidator validator : validators) { if (validator.supports(methodParameterResolver.getParamMetaDatas()[i])) { registeredValidators[i] = validator; break; } } } // 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("net.paoding.rose.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 (RoseStringUtil.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 (!RoseStringUtil.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(); @Override 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() { @Override 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() { @Override 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[] {}); } @Override 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 = Arrays.copyOf(values, values.length + 1); values[values.length - 1] = value; } map.put(key, values); } @Override public Object execute(Rose rose) throws Throwable { try { return innerExecute(rose); } catch (Throwable local) { throw createException(rose, local); } } protected Object innerExecute(Rose rose) throws Throwable { Invocation inv = rose.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; } } } // 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(rose).doNext(); } private class InvocationChainImpl implements InvocationChain { private final boolean debugEnabled = logger.isDebugEnabled(); private int index = -1; private final Rose rose; private Object instruction; public InvocationChainImpl(Rose rose) { this.rose = rose; } @Override public Object doNext() throws Exception { if (++index < interceptors.length) { // ++index -10 InterceptorDelegate interceptor = interceptors[index]; // rose.addAfterCompletion(interceptor); Object instruction = interceptor.roundInvocation(rose.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(rose.getInvocation()); } this.instruction = method.invoke(controller, rose.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(Rose rose, Throwable exception) { final RequestPath requestPath = rose.getInvocation().getRequestPath(); StringBuilder sb = new StringBuilder(1024); sb.append("[Rose-").append(RoseVersion.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(rose.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() { } }