net.sf.cocmvc.ConventionalHandlerMapping.java Source code

Java tutorial

Introduction

Here is the source code for net.sf.cocmvc.ConventionalHandlerMapping.java

Source

/*
 * Copyright 2012 yingxinwu.g@gmail.com.
 *
 * 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.sf.cocmvc;

import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.condition.*;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import net.sf.cocmvc.annotation.NoMapping;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

import static org.springframework.core.annotation.AnnotationUtils.findAnnotation;

/**
 * Convention over configuration HandlerMapping<br/>
 *
 * Beans following naming convention, say "XxxController", will be treated as Controllers,
 * and each {@code public} method of a Controller will be mapping to an URL request
 *
 * <p>The default URL pattern is: {@code /<controller>/<action>}.<p/>
 *
 * @author ywu
 */
public class ConventionalHandlerMapping extends RequestMappingHandlerMapping {

    /**
     * Controller naming convention, default is 'XxxController'
     */
    private String controllerNameSuffix = "Controller";

    /**
     * Use snake-case URL, default is false (use camel-case)
     */
    private boolean useSnakeCase = false;

    /**
     * Whether to create mapping for annotationed methods, default is true, set to false if producing duplicated mappings
     */
    private boolean mapAnnotationedMethod = true;

    /**
     * Base package to compute the module name
     */
    private String basePackage;

    /**
     * Controller naming convention, the suffix will be removed before producing mapping, default is 'Controller'
     */
    public void setControllerNameSuffix(String controllerNameSuffix) {
        this.controllerNameSuffix = controllerNameSuffix;
    }

    /**
     * Use snake-case URL, default is false (use camel-case)
     */
    public void setUseSnakeCase(boolean useSnakeCase) {
        this.useSnakeCase = useSnakeCase;
    }

    /**
     * Whether to create mapping for annotationed methods, default is true, set to false if producing duplicated mappings
     */
    public void setMapAnnotationedMethod(boolean mapAnnotationedMethod) {
        this.mapAnnotationedMethod = mapAnnotationedMethod;
    }

    /**
     * Base package to compute the module name
     */
    public void setBasePackage(String basePackage) {
        this.basePackage = basePackage;
    }

    @Override
    protected boolean isHandler(Class<?> beanType) {
        return findAnnotation(beanType, NoMapping.class) == null
                && (beanType.getSimpleName().endsWith(controllerNameSuffix) || super.isHandler(beanType));
    }

    @Override
    protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
        if (!isAction(method))
            return null;

        // create mapping info for method and type level, and combine them
        RequestMappingInfo actionMapping = createActionMapping(method);
        return actionMapping != null ? createControllerMapping(handlerType).combine(actionMapping) : null;
    }

    /*
     * determine whether the method should be treated as an Action
     */
    private boolean isAction(Method method) {
        String name = method.getName();
        return Modifier.isPublic(method.getModifiers())
                && !(name.matches("^(equals|hashCode|toString|clone|notify.*|wait|getClass)") // methods declared in Object
                        || name.matches("^(init|destroy)") // common lifecycle methods
                        || name.matches("^([sg]et(MetaClass|Property)|.*\\$.*|invokeMethod)") // methods of groovy object
                        || findAnnotation(method, NoMapping.class) != null);
    }

    /*
     * create method-level mapping (an Action)
     */
    private RequestMappingInfo createActionMapping(Method method) {
        RequestMapping mapping = findAnnotation(method, RequestMapping.class);
        if (mapping == null)
            return createConventionalActionMapping(method);
        else
            return mapAnnotationedMethod ? createActionMapping(mapping, method) : null;
    }

    private RequestMappingInfo createConventionalActionMapping(Method method) {
        return new RequestMappingInfo(createPatternRequestCondition(buildConventionalActions(method)), null, null,
                null, null, null, getCustomMethodCondition(method));
    }

    private String[] buildConventionalActions(Method method) {
        // method named 'index' will be mapped to '/'
        String action = "index".equals(method.getName()) ? "" : method.getName();
        return new String[] { useSnakeCase ? toSnakeCase(action) : toCamelCase(action) };
    }

    private RequestMappingInfo createActionMapping(RequestMapping mapping, Method method) {
        return createRequestMapping(mapping, getCustomMethodCondition(method));
    }

    /*
     * create type-level mapping (a Controller)
     */
    private RequestMappingInfo createControllerMapping(Class<?> handlerType) {
        RequestMapping mapping = findAnnotation(handlerType, RequestMapping.class);
        return mapping == null ? createConventionalControllerMapping(handlerType)
                : createControllerMapping(mapping, handlerType);
    }

    private RequestMappingInfo createConventionalControllerMapping(Class handlerType) {
        return new RequestMappingInfo(createPatternRequestCondition(buildConventionalControllerPaths(handlerType)),
                null, null, null, null, null, getCustomTypeCondition(handlerType));
    }

    private String[] buildConventionalControllerPaths(Class handlerType) {
        String prefix = determineControllerPrefix(handlerType);
        String path = handlerType.getSimpleName();
        if (path.endsWith(controllerNameSuffix))
            path = path.substring(0, path.lastIndexOf(controllerNameSuffix));
        return new String[] { prefix + '/' + (useSnakeCase ? toSnakeCase(path) : toCamelCase(path)) };
    }

    private RequestMappingInfo createControllerMapping(RequestMapping mapping, Class<?> handlerType) {
        return createRequestMapping(mapping, getCustomTypeCondition(handlerType));
    }

    private PatternsRequestCondition createPatternRequestCondition(String[] patterns) {
        return new PatternsRequestCondition(patterns, this.getUrlPathHelper(), this.getPathMatcher(),
                useSuffixPatternMatch(), useTrailingSlashMatch());
    }

    private RequestMappingInfo createRequestMapping(RequestMapping annotation,
            RequestCondition<?> customCondition) {
        return new RequestMappingInfo(createPatternRequestCondition(annotation.value()),
                new RequestMethodsRequestCondition(annotation.method()),
                new ParamsRequestCondition(annotation.params()), new HeadersRequestCondition(annotation.headers()),
                new ConsumesRequestCondition(annotation.consumes(), annotation.headers()),
                new ProducesRequestCondition(annotation.produces(), annotation.headers()), customCondition);
    }

    private String determineControllerPrefix(Class<?> handlerType) {
        if (basePackage == null)
            return "";

        String relativePackage = handlerType.getPackage().getName().replaceFirst(basePackage, "");
        return packageToPath(relativePackage);
    }

    private String packageToPath(String pack) {
        String path = pack.replaceAll("\\.", "/");
        return useSnakeCase ? toSnakeCase(path) : toCamelCase(path);
    }

    private String toSnakeCase(String str) {
        return str.replaceAll("([A-Z])", "-$1").toLowerCase().replaceAll("^-", "");
    }

    private String toCamelCase(String str) {
        if (!StringUtils.hasText(str))
            return str;
        return Character.toLowerCase(str.charAt(0)) + (str.length() > 1 ? str.substring(1) : "");
    }

}