Package cn.webwheel

Source Code of cn.webwheel.Main$ActionImpl

/*
* Copyright 2012 XueSong Guo.
*
* 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 cn.webwheel;

import cn.webwheel.results.SimpleResult;
import cn.webwheel.results.SimpleResultInterpreter;
import cn.webwheel.results.TemplateResult;
import cn.webwheel.results.TemplateResultInterpreter;
import cn.webwheel.setters.*;

import javax.servlet.FilterChain;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.net.URL;
import java.net.URLDecoder;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

/**
* Entrance of WebWheel MVC. There must be a class inherit this abstract class in each WebWheel application.<br/
* Subclass must have default constructor(no argument), for being instantiated.
*/
abstract public class Main {

    /**
     * current servlet context
     */
    protected ServletContext servletContext;

    protected Map<Class, ResultInterpreter> interpreterMap = new HashMap<Class, ResultInterpreter>();

    protected Map<String, ActionInfo> actionMap = new HashMap<String, ActionInfo>();

    protected ActionSetter actionSetter = new ActionSetter();

    protected Map<Pattern, String> rewritePatterns = new HashMap<Pattern, String>();

    protected final SetterConfig defSetterConfig = new SetterConfig();

    /**
     * When action method is an instance method, action object is created by this method.
     * <p>
     * The default implement use {@link Class#newInstance()} to instantiate the action object, and set {@link WebContext} to it if action class inherited from {@link WebContextAware}.<br/>
     * Subclass can use IOC container to get action object.
     * @see WebContextAware
     * @see WebContext
     * @return action instance
     */
    public <T> T createAction(WebContext ctx, Class<T> type) {
        T action;
        try {
            action = type.newInstance();
        } catch (Exception e) {
            throw new RuntimeException("can not create instance of " + type, e);
        }
        if (action instanceof WebContextAware) {
            ((WebContextAware) action).setWebContext(ctx);
        }
        return action;
    }

    /**
     * WebWheel MVC application initializing method.<br/>
     * The default implement configures 2 {@link ResultInterpreter}s and 28 {@link Setter}s.<br/>
     * <p>
     * <b>result interpreter</b>:<br/>
     * {@link TemplateResult} to {@link TemplateResultInterpreter}<br/>
     * {@link SimpleResult} to {@link SimpleResultInterpreter}<br/>
     * <br/>
     * <b>http parameter binding setter</b>:<br/>
     * <br/>
     * String<br/>
     * String[]<br/>
     * <br/>
     * boolean<br/>
     * Boolean<br/>
     * boolean[]<br/>
     * <br/>
     * int<br/>
     * Integer<br/>
     * int[]<br/>
     * <br/>
     * long<br/>
     * Long<br/>
     * long[]<br/>
     * <br/>
     * float<br/>
     * Float<br/>
     * float[]<br/>
     * <br/>
     * double<br/>
     * Double<br/>
     * double[]<br/>
     * <br/>
     * File<br/>
     * File[]<br/>
     * <br/>
     * FileEx<br/>
     * FileEx[]<br/>
     * <br/>
     * Map&lt;String, Object><br/>
     * Map&lt;String, String><br/>
     * Map&lt;String, String[]><br/>
     * Map&lt;String, File><br/>
     * Map&lt;String, File[]><br/>
     * Map&lt;String, FileEx><br/>
     * Map&lt;String, FileEx[]>
     * @see FileEx
     */
    protected void init() throws ServletException {

        File root = new File(servletContext.getRealPath("/"));
        interpret(TemplateResult.class).by(new TemplateResultInterpreter(root, null));
        interpret(SimpleResult.class).by(new SimpleResultInterpreter());

        set(String.class).by(new StringSetter());
        set(String[].class).by(new StringArraySetter());

        set(boolean.class).by(new BooleanSetter(Boolean.FALSE));
        set(Boolean.class).by(new BooleanSetter(null));
        set(boolean[].class).by(new BooleanArraySetter());

        set(byte.class).by(new ByteSetter((byte) 0));
        set(Byte.class).by(new ByteSetter(null));
        set(byte[].class).by(new ByteArraySetter());

        set(short.class).by(new ShortSetter((short) 0));
        set(Short.class).by(new ShortSetter(null));
        set(short[].class).by(new ShortArraySetter());

        set(int.class).by(new IntSetter(0));
        set(Integer.class).by(new IntSetter(null));
        set(int[].class).by(new IntArraySetter());

        set(long.class).by(new LongSetter(0L));
        set(Long.class).by(new LongSetter(null));
        set(long[].class).by(new LongArraySetter());

        set(float.class).by(new FloatSetter(0f));
        set(Float.class).by(new FloatSetter(null));
        set(float[].class).by(new FloatArraySetter());

        set(double.class).by(new DoubleSetter(0.0));
        set(Double.class).by(new DoubleSetter(null));
        set(double[].class).by(new DoubleArraySetter());

        set(File.class).by(new FileSetter());
        set(File[].class).by(new FileArraySetter());

        set(FileEx.class).by(new FileExSetter());
        set(FileEx[].class).by(new FileExArraySetter());

        set(new TypeLiteral<Map<String, Object>>(){}.getType()).by(new MapOSetter());

        set(new TypeLiteral<Map<String, String>>(){}.getType()).by(new MapSSetter());
        set(new TypeLiteral<Map<String, String[]>>(){}.getType()).by(new MapSASetter());

        set(new TypeLiteral<Map<String, File>>(){}.getType()).by(new MapFSetter());
        set(new TypeLiteral<Map<String, File[]>>(){}.getType()).by(new MapFASetter());

        set(new TypeLiteral<Map<String, FileEx>>(){}.getType()).by(new MapFxSetter());
        set(new TypeLiteral<Map<String, FileEx[]>>(){}.getType()).by(new MapFxASetter());
    }

    /**
     * Destroy method. To be invoked in {@link cn.webwheel.WebWheelFilter#destroy()}
     */
    protected void destroy() {}

    /**
     * According action result's type, find appropriate interpreter to interpret the result instance.
     * <p>
     * The finding procedure is from bottom to up in class inheritance diagram.
     * @param ctx current context
     * @param result result object of action method
     * @return interpreter is found
     * @throws IOException
     * @throws ServletException
     */
    @SuppressWarnings("unchecked")
    protected boolean interpretResult(WebContext ctx, Object result) throws IOException, ServletException {
        if (result == null) {
            return false;
        }

        Class cls = result.getClass();
        do {
            ResultInterpreter it = interpreterMap.get(cls);
            if (it != null) {
                it.interpret(result, ctx);
                return true;
            }
            for (Class inf : cls.getInterfaces()) {
                it = interpreterMap.get(inf);
                if (it != null) {
                    it.interpret(result, ctx);
                    return true;
                }
            }
        } while ((cls = cls.getSuperclass()) != null);

        return false;
    }

    /**
     * Invoke action method.
     * @param ctx current context
     * @param ai action info
     * @param action action object, may be null for static action method
     * @return result of action method
     */
    protected Object executeAction(WebContext ctx, ActionInfo ai, Object action) throws Throwable {
        try {
            Object[] args = actionSetter.set(action, ai, ctx.getRequest());
            try {
                return ai.actionMethod.invoke(action == null ? ai.actionMethod.getDeclaringClass() : action, args);
            } catch (IllegalAccessException ignored) {
                return null;
            } catch (InvocationTargetException e) {
                throw e.getCause();
            }
        } finally {
            actionSetter.clear(ctx.getRequest());
        }
    }

    /**
     * Get http url(after http forward or include)
     */
    protected String pathFor(HttpServletRequest request) {
        String path = (String) request.getAttribute("javax.servlet.include.servlet_path");
        if (path != null) {
            String info = (String) request.getAttribute("javax.servlet.include.path_info");
            if (info != null) {
                path += info;
            }
        } else {
            path = request.getServletPath();
            String info = request.getPathInfo();
            if (info != null) {
                path += info;
            }
        }
        return path;
    }

    /**
     * Http request url rewriting.
     * @see ActionBinder#rest(String)
     * @param url http url
     * @return url after rewrite
     */
    protected String handleRewrite(String url) {
        for (Map.Entry<Pattern, String> entry : rewritePatterns.entrySet()) {
            Matcher matcher = entry.getKey().matcher(url);
            if (matcher.matches()) {
                return matcher.replaceAll(entry.getValue());
            }
        }
        return null;
    }

    /**
     * http request handling procedure.
     */
    @SuppressWarnings("unchecked")
    protected void process(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {

        String path = pathFor(request);

        String rewrite = handleRewrite(path);
        if (rewrite != null) {
            request.getRequestDispatcher(rewrite).forward(request, response);
            return;
        }

        ActionInfo ai = actionMap.get(path);
        if (ai == null) {
            filterChain.doFilter(request, response);
            return;
        }
        WebContextImpl ctx = new WebContextImpl(path, request, response);
        Object action = null;
        if (!Modifier.isStatic(ai.getActionMethod().getModifiers())) {
            action = createAction(ctx, ai.actionClass);
            if (action == null) {
                filterChain.doFilter(request, response);
                return;
            }
        }

        Object result;
        try {
            if (ai.getActionMethod().getReturnType() == void.class) {
                executeAction(ctx, ai, action);
                return;
            }
            result = executeAction(ctx, ai, action);
        } catch (RuntimeException e) {
            throw e;
        } catch (IOException e) {
            throw e;
        } catch (ServletException e) {
            throw e;
        } catch (Throwable e) {
            throw new ServletException(e);
        }

        boolean solved = interpretResult(ctx, result);

        if (!solved) {
            if (result == null) {
                filterChain.doFilter(request, response);
            } else {
                response.getWriter().write(result.toString());
            }
        }
    }

    /**
     * Map a http url to action method, optionally using url rewrite.
     * <p>
     * <b>example:</b><br/>
     * map("/index.html").with(PageIndex.class, "renderPage");
     * map("/article.html?id=$1").rest("/article/(\\d+)").with(Article.class, Article.class.getMethod("viewArticle"));
     * @see ActionBinder#rest(String)
     * @see ActionBinder#with(Class, String)
     * @see ActionBinder#with(Class, Method)
     * @param path http url
     * @return binder
     */
    protected ActionBinder map(String path) {
        return new ActionBinder(path);
    }

    protected class ActionBinder {

        private String path;

        private ActionBinder(String path) {
            this.path = path;
        }

        /**
         * Implement a simple url rewrite.
         * <p>
         * Using such procedure:<br/>
         * Pattern.compile(rest).matcher(url).replaceAll(path)
         * @param rest url pattern.
         * @return binder
         * @throws PatternSyntaxException rest pattern is wrong
         */
        public ActionBinder rest(String rest) throws PatternSyntaxException {
            Pattern pat = Pattern.compile(rest);
            for (Pattern p : rewritePatterns.keySet()) {
                if (p.pattern().equals(rest)) {
                    throw new IllegalArgumentException("duplicated rewrite path: " + rest);
                }
            }
            rewritePatterns.put(pat, path);
            return this;
        }

        /**
         * Map url to action method name.
         * <p>
         * There's must only one method named the parameter.
         * @param actionClass action class
         * @param methodName action method name
         * @return http parameter binding config
         */
        public SetterConfig with(Class actionClass, String methodName) throws NoSuchMethodException {
            Method m = null;
            for (Method mtd : actionClass.getMethods()) {
                if (mtd.getName().equals(methodName)) {
                    if (m != null) {
                        throw new NoSuchMethodException("duplicated method named: " + methodName + " in " + actionClass);
                    }
                    m = mtd;
                }
            }
            return with(actionClass, m);
        }

        /**
         * Map url to action method.
         * @param actionClass action class
         * @param method action method
         * @return http parameter binding config
         */
        @SuppressWarnings("unchecked")
        public SetterConfig with(Class actionClass, Method method) {
            if ((actionClass.isMemberClass() && !Modifier.isStatic(actionClass.getModifiers()))
                    || actionClass.isAnonymousClass() || actionClass.isLocalClass()
                    || !Modifier.isPublic(actionClass.getModifiers())
                    || Modifier.isAbstract(actionClass.getModifiers())) {
                throw new IllegalArgumentException("action class signature wrong: " + actionClass);
            }
            if (!Modifier.isPublic(method.getModifiers())) {
                throw new IllegalArgumentException("action method signature wrong: " + method);
            }
            ActionInfo ai = new ActionInfo(actionClass, method);
            int i = path.indexOf('?');
            String realPath = i == -1 ? path : path.substring(0, i);
            if (actionMap.put(realPath, ai) != null) {
                throw new RuntimeException("duplicated action for path: " + realPath);
            }
            return ai.setterConfig = new SetterConfig(defSetterConfig);
        }
    }

    /**
     * Map a result type to result interpreter.
     * @param resultType result class type
     * @param <T> result class type
     * @return binder
     */
    protected <T> ResultTypeBinder<T> interpret(Class<T> resultType) {
        return new ResultTypeBinder<T>(resultType);
    }

    /**
     * Map a type to http parameter binding setter.
     * @param type parameter type, may be generic type.
     * @see TypeLiteral
     * @return binder
     */
    protected SetterBinder set(Type type) {
        return new SetterBinder(type);
    }

    protected class SetterBinder {

        private Type type;

        private SetterBinder(Type type) {
            this.type = type;
        }

        /**
         * map http parameter binding setter
         */
        public void by(Setter setter) {
            actionSetter.addSetter(type, setter);
        }
    }

    /**
     * Find action method under certain package recursively.
     * <p>
     * Action method must be marked by {@link Action}(may be through parent class).<br/>
     * Url will be the package path under rootpkg.<br/>
     * <b>example</b><br/>
     * action class:
     * <p><blockquote><pre>
     *     package com.my.app.web.user;
     *     public class insert {
     *        {@code @}Action
     *         public Object act() {...}
     *     }
     * </pre></blockquote><p>
     * This action method will be mapped to url: /user/insert.act
     * @see #map(String)
     * @see Action
     * @param rootpkg action class package
     */
    @SuppressWarnings("deprecation")
    final protected void autoMap(String rootpkg) {
        try {
            Enumeration<URL> enm = getClass().getClassLoader().getResources(rootpkg.replace('.', '/'));
            while (enm.hasMoreElements()) {
                URL url = enm.nextElement();
                if (url.getProtocol().equals("file")) {
                    autoMap(rootpkg.replace('.', '/'), rootpkg, new File(URLDecoder.decode(url.getFile())));
                } else if (url.getProtocol().equals("jar")) {
                    String file = URLDecoder.decode(url.getFile());
                    String root = file.substring(file.lastIndexOf('!') + 2);
                    file = file.substring(0, file.length() - root.length() - 2);
                    URL jarurl = new URL(file);
                    if (jarurl.getProtocol().equals("file")) {
                        JarFile jarFile = new JarFile(URLDecoder.decode(jarurl.getFile()));
                        try {
                            Enumeration<JarEntry> entries = jarFile.entries();
                            while (entries.hasMoreElements()) {
                                JarEntry entry = entries.nextElement();
                                String name = entry.getName();
                                if (!name.endsWith(".class")) continue;
                                if (!name.startsWith(root + '/')) continue;
                                name = name.substring(0, name.length() - 6);
                                name = name.replace('/', '.');
                                int i = name.lastIndexOf('.');
                                autoMap(root, name.substring(0, i), name.substring(i + 1));
                            }
                        } finally {
                            jarFile.close();
                        }
                    }
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private void autoMap(String root, String pkg, File dir) {
        File[] files = dir.listFiles();
        if (files == null) return;
        for (File file : files) {
            String fn = file.getName();
            if (file.isDirectory()) {
                autoMap(root, pkg + "." + fn, file);
            } else if (fn.endsWith(".class")) {
                autoMap(root, pkg, fn.substring(0, fn.length() - 6));
            }
        }
    }

    private void getActions(List<Action> list, Set<Class> set, Class cls, Method method) {
        if (cls == null || !set.add(cls)) return;
        for (Method m : cls.getDeclaredMethods()) {
            if (!m.getName().equals(method.getName())) continue;
            if (!Arrays.equals(m.getParameterTypes(), method.getParameterTypes())) continue;
            Action action = m.getAnnotation(Action.class);
            if (action != null) {
                list.add(action);
            }
            break;
        }
        for (Class i : cls.getInterfaces()) {
            getActions(list, set, i, method);
        }
        getActions(list, set, cls.getSuperclass(), method);
    }

    private Action getAction(Class cls, Method method) {
        if (Modifier.isStatic(method.getModifiers())) {
            return method.getAnnotation(Action.class);
        }
        ArrayList<Action> actions = new ArrayList<Action>();
        getActions(actions, new HashSet<Class>(), cls, method);
        if (actions.isEmpty()) return null;
        if (actions.get(0).disabled()) return null;
        if (actions.size() == 1) return actions.get(0);
        ActionImpl action = new ActionImpl();
        for (Action act : actions) {
            if (action.merge(act)) {
                return action;
            }
        }
        return action;
    }

    @SuppressWarnings("unchecked")
    private void autoMap(String root, String pkg, String name) {
        Class cls;
        try {
            cls = Class.forName(pkg + "." + name);
        } catch (ClassNotFoundException e) {
            return;
        }
        if (cls.isMemberClass() && !Modifier.isStatic(cls.getModifiers())) {
            return;
        }
        if (cls.isAnonymousClass() || cls.isLocalClass()
                || !Modifier.isPublic(cls.getModifiers())
                || Modifier.isAbstract(cls.getModifiers())) {
            return;
        }

        name = name.replace('$', '.');

        for (Method method : cls.getMethods()) {

            String pathPrefix = pkg.substring(root.length()).replace('.', '/') + '/';
            String path = pathPrefix + name + '.' + method.getName();

            Action action = getAction(cls, method);
            if (action == null) continue;

            if (!action.value().isEmpty()) {
                if (action.value().startsWith("?")) {
                    path = path + action.value();
                } else if (action.value().startsWith(".")) {
                    path = pathPrefix + name + action.value();
                } else if (!action.value().startsWith("/")) {
                    path = pathPrefix + action.value();
                } else {
                    path = action.value();
                }
            }
            ActionBinder binder = map(path);
            if (!action.rest().isEmpty()) {
                binder = binder.rest(action.rest());
            }
            SetterConfig cfg = binder.with(cls, method);
            if (!action.charset().isEmpty()) {
                cfg = cfg.setCharset(action.charset());
            }
            if (action.fileUploadFileSizeMax() != 0) {
                cfg = cfg.setFileUploadFileSizeMax(action.fileUploadFileSizeMax());
            }
            if (action.fileUploadSizeMax() != 0) {
                cfg.setFileUploadSizeMax(action.fileUploadSizeMax());
            }
        }
    }

    private static class ActionImpl implements Action {

        String map = "";
        String rest = "";
        String charset = "";
        int fileUploadSizeMax;
        int fileUploadFileSizeMax;

        boolean merge(Action act) {
            if (act.disabled()) return true;
            if (map.isEmpty()) {
                map = act.value();
            }
            if (rest.isEmpty()) {
                rest = act.rest();
            }
            if (charset.isEmpty()) {
                charset = act.charset();
            }
            if (fileUploadSizeMax == 0) {
                fileUploadSizeMax = act.fileUploadSizeMax();
            }
            if (fileUploadFileSizeMax == 0) {
                fileUploadFileSizeMax = act.fileUploadFileSizeMax();
            }
            return !map.isEmpty() && !rest.isEmpty() && !charset.isEmpty() && fileUploadSizeMax != 0 && fileUploadFileSizeMax != 0;
        }

        @Override
        public boolean disabled() {
            return false;
        }

        @Override
        public String value() {
            return map;
        }

        @Override
        public String rest() {
            return rest;
        }

        @Override
        public String charset() {
            return charset;
        }

        @Override
        public int fileUploadSizeMax() {
            return fileUploadSizeMax;
        }

        @Override
        public int fileUploadFileSizeMax() {
            return fileUploadFileSizeMax;
        }

        @Override
        public Class<? extends Annotation> annotationType() {
            return Action.class;
        }
    }

    protected class ResultTypeBinder<T> {

        private Class<T> resultType;

        private ResultTypeBinder(Class<T> resultType) {
            this.resultType = resultType;
        }

        /**
         * map result interpreter
         */
        public void by(ResultInterpreter<? extends T> interpreterClass) {
            if (interpreterClass == null) {
                interpreterMap.remove(resultType);
            } else {
                interpreterMap.put(resultType, interpreterClass);
            }
        }
    }

    private class WebContextImpl implements WebContext {

        private String path;
        private HttpServletRequest request;
        private HttpServletResponse response;

        private WebContextImpl(String path, HttpServletRequest request, HttpServletResponse response) {
            this.path = path;
            this.request = request;
            this.response = response;
        }

        @Override
        public String getPath() {
            return path;
        }

        @Override
        public Main getMain() {
            return Main.this;
        }

        @Override
        public ServletContext getContext() {
            return servletContext;
        }

        @Override
        public HttpServletRequest getRequest() {
            return request;
        }

        @Override
        public HttpServletResponse getResponse() {
            return response;
        }
    }
}
TOP

Related Classes of cn.webwheel.Main$ActionImpl

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.