001/**
002 * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com).
003 * <p>
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 * <p>
008 * http://www.apache.org/licenses/LICENSE-2.0
009 * <p>
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package io.jboot.web;
017
018import com.google.common.base.Joiner;
019import com.jfinal.aop.Interceptor;
020import com.jfinal.aop.InterceptorManager;
021import com.jfinal.config.Routes;
022import com.jfinal.core.Action;
023import com.jfinal.core.ActionKey;
024import com.jfinal.core.Controller;
025import com.jfinal.core.NotAction;
026import io.jboot.utils.AntPathMatcher;
027import io.jboot.utils.ArrayUtil;
028
029import java.lang.reflect.Method;
030import java.lang.reflect.Modifier;
031import java.lang.reflect.ParameterizedType;
032import java.lang.reflect.Type;
033import java.util.Map;
034import java.util.concurrent.ConcurrentHashMap;
035
036/**
037 * @author 没牙的小朋友 (mjl@nxu.edu.cn)
038 * @version V1.0
039 */
040public class PathVariableActionMapping extends JbootActionMapping {
041    private static final String PATH_VARIABLE_URL_PATTERN = ".*\\{[a-zA-Z0-9]+\\}.*";
042    protected Map<String, Action> pathVariableUrlMapping = new ConcurrentHashMap<>();
043    private static final AntPathMatcher antPathMatcher = new AntPathMatcher();
044
045    public PathVariableActionMapping(Routes routes) {
046        super(routes);
047    }
048
049    @Override
050    protected void buildActionMapping() {
051        mapping.clear();
052        Class<?> dc;
053        InterceptorManager interMan = InterceptorManager.me();
054        for (Routes routes : getRoutesList()) {
055            for (Routes.Route route : routes.getRouteItemList()) {
056                Class<? extends Controller> controllerClass = route.getControllerClass();
057                Interceptor[] controllerInters = interMan.createControllerInterceptor(controllerClass);
058
059                boolean declaredMethods = !routes.getMappingSuperClass() || controllerClass.getSuperclass() == Controller.class;
060
061                Method[] methods = (declaredMethods ? controllerClass.getDeclaredMethods() : controllerClass.getMethods());
062                for (Method method : methods) {
063                    if (declaredMethods) {
064                        if (!Modifier.isPublic(method.getModifiers()))
065                            continue;
066                    } else {
067                        dc = method.getDeclaringClass();
068                        if (dc == Controller.class || dc == Object.class)
069                            continue;
070                    }
071
072                    if (method.getAnnotation(NotAction.class) != null) {
073                        continue;
074                    }
075
076                    Interceptor[] actionInters = interMan.buildControllerActionInterceptor(routes.getInterceptors(), controllerInters, controllerClass, method);
077                    String controllerPath = route.getControllerPath();
078
079                    String methodName = method.getName();
080                    ActionKey ak = method.getAnnotation(ActionKey.class);
081                    String actionKey;
082                    if (ak != null) {
083                        actionKey = ak.value().trim();
084                        if ("".equals(actionKey)) {
085                            throw new IllegalArgumentException(controllerClass.getName() + "." + methodName + "(): The argument of ActionKey can not be blank.");
086                        }
087                        if (actionKey.matches(PATH_VARIABLE_URL_PATTERN)) {
088                            Action pathVariableAction = new Action(controllerPath, actionKey, controllerClass, method, methodName, actionInters,
089                                    route.getFinalViewPath(routes.getBaseViewPath()));
090                            pathVariableUrlMapping.put(actionKey, pathVariableAction);
091                        }
092                        if (actionKey.startsWith(SLASH)) {
093                            //actionKey = actionKey
094                        } else if (actionKey.startsWith("./")) {
095                            actionKey = controllerPath + actionKey.substring(1);
096                        } else {
097                            actionKey = SLASH + actionKey;
098                        }
099//                        if (!actionKey.startsWith(SLASH)) {
100//                            actionKey = SLASH + actionKey;
101//                        }
102                    } else if (methodName.equals("index")) {
103                        actionKey = controllerPath;
104                    } else {
105                        actionKey = controllerPath.equals(SLASH) ? SLASH + methodName : controllerPath + SLASH + methodName;
106                    }
107
108//                    Action action = new Action(controllerPath, actionKey, controllerClass, method, methodName, actionInters, route.getFinalViewPath(routes.getBaseViewPath()));
109//                    if (mapping.put(actionKey, action) != null) {
110//                        throw new RuntimeException(buildMsg(actionKey, controllerClass, method));
111//                    }
112
113                    Action newAction = new Action(controllerPath, actionKey, controllerClass, method, methodName, actionInters, route.getFinalViewPath(routes.getBaseViewPath()));
114                    Action existAction = mapping.get(actionKey);
115                    if (existAction == null) {
116                        mapping.put(actionKey, newAction);
117                    } else {
118
119                        Type controllerType = controllerClass.getGenericSuperclass();
120                        Method existActionMethod = existAction.getMethod();
121
122                        // 不是泛型
123                        if (!(controllerType instanceof ParameterizedType)) {
124                            throw new RuntimeException(buildMsg(actionKey, method, existActionMethod));
125                        }
126
127                        if (method.getParameterCount() == 0
128                                || method.getParameterCount() != existActionMethod.getParameterCount()
129                                || method.getDeclaringClass() != existActionMethod.getDeclaringClass()) {
130                            throw new RuntimeException(buildMsg(actionKey, method, existActionMethod));
131                        }
132
133                        Type[] argumentTypes = ((ParameterizedType) controllerType).getActualTypeArguments();
134
135                        Class<?>[] paraTypes = method.getParameterTypes();
136                        Class<?>[] existParaTypes = existActionMethod.getParameterTypes();
137
138                        for (int i = 0; i < paraTypes.length; i++) {
139                            Class<?> newType = paraTypes[i];
140                            Class<?> existType = existParaTypes[i];
141                            if (newType == existType) {
142                                continue;
143                            }
144                            // newType 是父类
145                            else if (newType.isAssignableFrom(existType) && ArrayUtil.contains(argumentTypes, existType)) {
146                                break;
147                            }
148                            // newType 是子类
149                            else if (existType.isAssignableFrom(newType) && ArrayUtil.contains(argumentTypes, newType)) {
150                                mapping.put(actionKey, newAction);
151                                break;
152                            } else {
153                                throw new RuntimeException(buildMsg(actionKey, method, existActionMethod));
154                            }
155                        }
156
157                    }
158                }
159            }
160        }
161        routes.clear();
162
163        // support url = controllerPath + urlParas with "/" of controllerPath
164        Action action = mapping.get("/");
165        if (action != null) {
166            mapping.put("", action);
167        }
168    }
169
170
171
172
173    /**
174     * Support four types of url
175     * 1: http://abc.com/controllerPath                 ---> 00
176     * 2: http://abc.com/controllerPath/para            ---> 01
177     * 3: http://abc.com/controllerPath/method          ---> 10
178     * 4: http://abc.com/controllerPath/method/para     ---> 11
179     * 5: http://abc.com/foo/{id}/bar/{name}
180     * The controllerPath can also contains "/"
181     * Example: http://abc.com/uvw/xyz/method/para
182     */
183    @Override
184    public Action getAction(String url, String[] urlPara) {
185        Action action = mapping.get(url);
186        if (action != null) {
187            return action;
188        }
189        for (String pattern : pathVariableUrlMapping.keySet()) {
190            //判断是否有匹配包含路径参数的URL映射
191            if (antPathMatcher.match(pattern, url)) {
192                Action pathVariableUrlAction = pathVariableUrlMapping.get(pattern);
193                Map<String, String> pathVariableValues = antPathMatcher.extractUriTemplateVariables(pattern, url);
194                urlPara[0] = null;
195                if (urlPara.length > 1) {
196                    //urlPara[1]作为路径参数传入controller
197                    urlPara[1] = Joiner.on("&").withKeyValueSeparator("=").join(pathVariableValues);
198                }
199                return pathVariableUrlAction;
200            }
201        }
202        // --------
203        int i = url.lastIndexOf('/');
204        if (i != -1) {
205            action = mapping.get(url.substring(0, i));
206            if (action != null) {
207                urlPara[0] = url.substring(i + 1);
208            }
209        }
210
211        return action;
212    }
213}