com.revolsys.ui.web.rest.interceptor.WebAnnotationMethodHandlerAdapter.java Source code

Java tutorial

Introduction

Here is the source code for com.revolsys.ui.web.rest.interceptor.WebAnnotationMethodHandlerAdapter.java

Source

/*
 * Copyright 2002-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.revolsys.ui.web.rest.interceptor;

import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

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

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.core.Conventions;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.MethodParameter;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.ui.ExtendedModelMap;
import org.springframework.ui.Model;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.PathMatcher;
import org.springframework.validation.support.BindingAwareModelMap;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.bind.ServletRequestDataBinder;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.servlet.HandlerAdapter;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.mvc.annotation.ModelAndViewResolver;
import org.springframework.web.servlet.mvc.multiaction.InternalPathMethodNameResolver;
import org.springframework.web.servlet.mvc.multiaction.MethodNameResolver;
import org.springframework.web.servlet.support.WebContentGenerator;
import org.springframework.web.util.UrlPathHelper;

import com.revolsys.io.IoConstants;
import com.revolsys.ui.web.annotation.RequestMapping;
import com.revolsys.ui.web.utils.HttpServletUtils;

/**
 * Implementation of the {@link org.springframework.web.servlet.HandlerAdapter}
 * interface that maps handler methods based on HTTP paths, HTTP methods and
 * request parameters expressed through the {@link RequestMapping} annotation.
 * <p>
 * Supports request parameter binding through the {@link RequestParam}
 * annotation. Also supports the {@link ModelAttribute} annotation for exposing
 * model attribute values to the view, as well as {@link InitBinder} for binder
 * initialization methods and {@link SessionAttributes} for automatic session
 * management of specific attributes.
 * <p>
 * This adapter can be customized through various bean properties. A common use
 * case is to apply shared binder initialization logic through a custom
 * {@link #setWebBindingInitializer WebBindingInitializer}.
 *
 * @author Juergen Hoeller
 * @author Arjen Poutsma
 * @see #setPathMatcher
 * @see #setMethodNameResolver
 * @see #setWebBindingInitializer
 * @see #setSessionAttributeStore
 * @since 2.5
 */
public class WebAnnotationMethodHandlerAdapter extends WebContentGenerator implements HandlerAdapter, Ordered {

    /**
     * Log category to use when no mapped handler is found for a request.
     *
     * @see #pageNotFoundLogger
     */
    public static final String PAGE_NOT_FOUND_LOG_CATEGORY = "org.springframework.web.servlet.PageNotFound";

    /**
     * Additional logger to use when no mapped handler is found for a request.
     *
     * @see #PAGE_NOT_FOUND_LOG_CATEGORY
     */
    protected static final Log pageNotFoundLogger = LogFactory.getLog(PAGE_NOT_FOUND_LOG_CATEGORY);

    private ModelAndViewResolver[] customModelAndViewResolvers;

    protected MediaType defaultMediaType;

    protected List<String> mediaTypeOrder = Arrays.asList("attribute", "parameter", "fileName", "pathExtension",
            "acceptHeader", "defaultMediaType");

    protected final ConcurrentMap<String, MediaType> mediaTypes = new ConcurrentHashMap<>();

    protected HttpMessageConverter<?>[] messageConverters = new HttpMessageConverter[] {
            new ByteArrayHttpMessageConverter(), new StringHttpMessageConverter(), new FormHttpMessageConverter() };

    protected MethodNameResolver methodNameResolver = new InternalPathMethodNameResolver();

    private final Map<Class<?>, AnnotationHandlerMethodResolver> methodResolverCache = new ConcurrentHashMap<>();

    private int order = Ordered.LOWEST_PRECEDENCE;

    protected String parameterName = "format";

    protected PathMatcher pathMatcher = new AntPathMatcher();

    protected UrlPathHelper urlPathHelper = new UrlPathHelper();

    public WebAnnotationMethodHandlerAdapter() {
        super(false);
    }

    protected final void addReturnValueAsModelAttribute(final Method handlerMethod, final Class<?> handlerType,
            final Object returnValue, final ExtendedModelMap implicitModel) {

        final ModelAttribute attr = AnnotationUtils.findAnnotation(handlerMethod, ModelAttribute.class);
        String attrName = attr != null ? attr.value() : "";
        if ("".equals(attrName)) {
            final Class<?> resolvedType = GenericTypeResolver.resolveReturnType(handlerMethod, handlerType);
            attrName = Conventions.getVariableNameForReturnType(handlerMethod, resolvedType, returnValue);
        }
        implicitModel.addAttribute(attrName, returnValue);
    }

    /**
     * Template method for creating a new ServletRequestDataBinder instance.
     * <p>
     * The default implementation creates a standard ServletRequestDataBinder.
     * This can be overridden for custom ServletRequestDataBinder subclasses.
     *
     * @param request current HTTP request
     * @param target the target object to bind onto (or <code>null</code> if the
     *          binder is just used to convert a plain parameter value)
     * @param objectName the objectName of the target object
     * @return the ServletRequestDataBinder instance to use
     * @throws Exception in case of invalid state or arguments
     * @see ServletRequestDataBinder#bind(javax.servlet.ServletRequest)
     * @see ServletRequestDataBinder#convertIfNecessary(Object, Class,
     *      MethodParameter)
     */
    protected ServletRequestDataBinder createBinder(final HttpServletRequest request, final Object target,
            final String objectName) throws Exception {

        return new ServletRequestDataBinder(target, objectName);
    }

    protected WebDataBinder createBinder(final NativeWebRequest webRequest, final Object target,
            final String objectName) throws Exception {

        return createBinder((HttpServletRequest) webRequest.getNativeRequest(), target, objectName);
    }

    @Override
    public long getLastModified(final HttpServletRequest request, final Object handler) {
        return -1;
    }

    private MediaType getMediaType(final List<MediaType> supportedMediaTypes, final MediaType acceptedMediaType) {
        for (final MediaType mediaType : supportedMediaTypes) {
            if (mediaType.equals(acceptedMediaType)) {
                return mediaType;
            }
        }
        for (final MediaType mediaType : supportedMediaTypes) {
            if (acceptedMediaType.isWildcardType() || mediaType.includes(acceptedMediaType)) {
                return mediaType;
            }
        }
        return null;
    }

    public List<String> getMediaTypeOrder() {
        return this.mediaTypeOrder;
    }

    /**
     * Return the message body converters that this adapter has been configured
     * with.
     */
    public HttpMessageConverter<?>[] getMessageConverters() {
        return this.messageConverters;
    }

    /**
     * Build a HandlerMethodResolver for the given handler type.
     */
    private AnnotationHandlerMethodResolver getMethodResolver(final Object handler) {
        final Class<?> handlerClass = ClassUtils.getUserClass(handler);
        AnnotationHandlerMethodResolver resolver = this.methodResolverCache.get(handlerClass);
        if (resolver == null) {
            resolver = new AnnotationHandlerMethodResolver(this, handlerClass);
            this.methodResolverCache.put(handlerClass, resolver);
        }
        return resolver;
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    public ModelAndView getModelAndView(final Method handlerMethod, final Class<?> handlerType,
            final Object returnValue, final ExtendedModelMap implicitModel, final ServletWebRequest webRequest)
            throws Exception {
        boolean responseArgumentUsed = false;
        final ResponseStatus responseStatusAnn = AnnotationUtils.findAnnotation(handlerMethod,
                ResponseStatus.class);
        if (responseStatusAnn != null) {
            final HttpStatus responseStatus = responseStatusAnn.value();
            // to be picked up by the RedirectView
            webRequest.getRequest().setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, responseStatus);
            webRequest.getResponse().setStatus(responseStatus.value());
            responseArgumentUsed = true;
        }

        // Invoke custom resolvers if present...
        if (WebAnnotationMethodHandlerAdapter.this.customModelAndViewResolvers != null) {
            for (final ModelAndViewResolver mavResolver : WebAnnotationMethodHandlerAdapter.this.customModelAndViewResolvers) {
                final ModelAndView mav = mavResolver.resolveModelAndView(handlerMethod, handlerType, returnValue,
                        implicitModel, webRequest);
                if (mav != ModelAndViewResolver.UNRESOLVED) {
                    return mav;
                }
            }
        }

        if (returnValue != null && AnnotationUtils.findAnnotation(handlerMethod, ResponseBody.class) != null) {
            final View view = handleResponseBody(returnValue, webRequest);
            return new ModelAndView(view).addAllObjects(implicitModel);
        }

        if (returnValue instanceof ModelAndView) {
            final ModelAndView mav = (ModelAndView) returnValue;
            mav.getModelMap().mergeAttributes(implicitModel);
            return mav;
        } else if (returnValue instanceof Model) {
            return new ModelAndView().addAllObjects(implicitModel).addAllObjects(((Model) returnValue).asMap());
        } else if (returnValue instanceof View) {
            return new ModelAndView((View) returnValue).addAllObjects(implicitModel);
        } else if (AnnotationUtils.findAnnotation(handlerMethod, ModelAttribute.class) != null) {
            addReturnValueAsModelAttribute(handlerMethod, handlerType, returnValue, implicitModel);
            return new ModelAndView().addAllObjects(implicitModel);
        } else if (returnValue instanceof Map) {
            return new ModelAndView().addAllObjects(implicitModel).addAllObjects((Map) returnValue);
        } else if (returnValue instanceof String) {
            return new ModelAndView((String) returnValue).addAllObjects(implicitModel);
        } else if (returnValue == null) {
            // Either returned null or was 'void' return.
            if (responseArgumentUsed || webRequest.isNotModified()) {
                return null;
            } else {
                // Assuming view name translation...
                return new ModelAndView().addAllObjects(implicitModel);
            }
        } else if (!BeanUtils.isSimpleProperty(returnValue.getClass())) {
            // Assume a single model attribute...
            addReturnValueAsModelAttribute(handlerMethod, handlerType, returnValue, implicitModel);
            return new ModelAndView().addAllObjects(implicitModel);
        } else {
            throw new IllegalArgumentException("Invalid handler method return value: " + returnValue);
        }
    }

    @Override
    public int getOrder() {
        return this.order;
    }

    @Override
    public ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response,
            final Object handler) throws Exception {
        final HttpServletRequest savedRequest = HttpServletUtils.getRequest();
        final HttpServletResponse savedResponse = HttpServletUtils.getResponse();
        try {
            HttpServletUtils.setRequestAndResponse(request, response);
            checkAndPrepare(request, response, true);

            return invokeHandlerMethod(request, response, handler);
        } finally {
            if (savedRequest == null) {
                HttpServletUtils.clearRequestAndResponse();
            } else {
                HttpServletUtils.setRequestAndResponse(savedRequest, savedResponse);
            }
        }
    }

    private View handleResponseBody(final Object returnValue, final ServletWebRequest webRequest)
            throws ServletException, IOException {

        final HttpServletRequest request = webRequest.getRequest();
        String jsonp = request.getParameter("jsonp");
        if (jsonp == null) {
            jsonp = request.getParameter("callback");
        }
        request.setAttribute(IoConstants.JSONP_PROPERTY, jsonp);
        List<MediaType> acceptedMediaTypes = MediaTypeUtil.getAcceptedMediaTypes(request,
                WebAnnotationMethodHandlerAdapter.this.mediaTypes,
                WebAnnotationMethodHandlerAdapter.this.mediaTypeOrder,
                WebAnnotationMethodHandlerAdapter.this.urlPathHelper,
                WebAnnotationMethodHandlerAdapter.this.parameterName,
                WebAnnotationMethodHandlerAdapter.this.defaultMediaType);
        if (acceptedMediaTypes.isEmpty()) {
            acceptedMediaTypes = Collections.singletonList(MediaType.ALL);
        }
        final Class<?> returnValueType = returnValue.getClass();
        final Set<MediaType> allSupportedMediaTypes = new LinkedHashSet<>();
        if (WebAnnotationMethodHandlerAdapter.this.messageConverters != null) {
            for (final MediaType acceptedMediaType : acceptedMediaTypes) {
                for (final HttpMessageConverter<?> messageConverter : WebAnnotationMethodHandlerAdapter.this.messageConverters) {
                    allSupportedMediaTypes.addAll(messageConverter.getSupportedMediaTypes());
                    if (messageConverter.canWrite(returnValueType, acceptedMediaType)) {
                        final MediaType mediaType = getMediaType(messageConverter.getSupportedMediaTypes(),
                                acceptedMediaType);
                        return new HttpMessageConverterView(messageConverter, mediaType, returnValue);
                    }
                }
            }
        }
        throw new HttpMediaTypeNotAcceptableException(new ArrayList<>(allSupportedMediaTypes));
    }

    protected ModelAndView invokeHandlerMethod(final HttpServletRequest request, final HttpServletResponse response,
            final Object handler) throws Exception {

        final AnnotationHandlerMethodResolver methodResolver = getMethodResolver(handler);
        final WebMethodHandler handlerMethod = methodResolver.resolveHandlerMethod(request);
        final ServletWebRequest webRequest = new ServletWebRequest(request, response);
        final RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        try {
            RequestContextHolder.setRequestAttributes(webRequest);
            final ExtendedModelMap implicitModel = new BindingAwareModelMap();

            final Object result = handlerMethod.invokeMethod(handler, request, response);
            if (result == null) {
                return null;
            } else {
                final ModelAndView mav = getModelAndView(handlerMethod.getMethod(), handler.getClass(), result,
                        implicitModel, webRequest);
                return mav;
            }
        } finally {
            RequestContextHolder.setRequestAttributes(requestAttributes);
        }
    }

    /**
     * Determine whether the given value qualifies as a "binding candidate", i.e. might potentially be subject to
     * bean-style data binding later on.
     */
    protected boolean isBindingCandidate(final Object value) {
        return value != null && !value.getClass().isArray() && !(value instanceof Collection)
                && !(value instanceof Map) && !BeanUtils.isSimpleValueType(value.getClass());
    }

    /**
     * Set if URL lookup should always use the full path within the current
     * servlet context. Else, the path within the current servlet mapping is used
     * if applicable (that is, in the case of a ".../*" servlet mapping in
     * web.xml).
     * <p>
     * Default is "false".
     *
     * @see org.springframework.web.util.UrlPathHelper#setAlwaysUseFullPath
     */
    public void setAlwaysUseFullPath(final boolean alwaysUseFullPath) {
        this.urlPathHelper.setAlwaysUseFullPath(alwaysUseFullPath);
    }

    /**
     * Set a custom ModelAndViewResolvers to use for special method return types.
     * <p>
     * Such a custom ModelAndViewResolver will kick in first, having a chance to
     * resolve a return value before the standard ModelAndView handling kicks in.
     */
    public void setCustomModelAndViewResolver(final ModelAndViewResolver customModelAndViewResolver) {
        this.customModelAndViewResolvers = new ModelAndViewResolver[] { customModelAndViewResolver };
    }

    /**
     * Set one or more custom ModelAndViewResolvers to use for special method
     * return types.
     * <p>
     * Any such custom ModelAndViewResolver will kick in first, having a chance to
     * resolve a return value before the standard ModelAndView handling kicks in.
     */
    public void setCustomModelAndViewResolvers(final ModelAndViewResolver[] customModelAndViewResolvers) {
        this.customModelAndViewResolvers = customModelAndViewResolvers;
    }

    /**
     * Sets the default content type.
     * <p>
     * This content type will be used when file extension, parameter, nor
     * {@code Accept} header define a content-type, either through being disabled
     * or empty.
     */
    public void setDefaultMediaType(final MediaType defaultContentType) {
        this.defaultMediaType = defaultContentType;
    }

    public void setMediaTypeOrder(final List<String> mediaTypeOrder) {
        this.mediaTypeOrder = mediaTypeOrder;
    }

    /**
     * Sets the mapping from file extensions to media types.
     * <p>
     */
    public void setMediaTypes(final Map<String, String> mediaTypes) {
        for (final Map.Entry<String, String> entry : mediaTypes.entrySet()) {
            final String extension = entry.getKey().toLowerCase(Locale.ENGLISH);
            final MediaType mediaType = MediaType.parseMediaType(entry.getValue());
            this.mediaTypes.put(extension, mediaType);
        }
    }

    /**
     * Set the message body converters to use.
     * <p>
     * These converters are used to convert from and to HTTP requests and
     * responses.
     */
    public void setMessageConverters(final HttpMessageConverter<?>[] messageConverters) {
        this.messageConverters = messageConverters;
    }

    /**
     * Set the MethodNameResolver to use for resolving default handler methods
     * (carrying an empty <code>@RequestMapping</code> annotation).
     * <p>
     * Will only kick in when the handler method cannot be resolved uniquely
     * through the annotation metadata already.
     */
    public void setMethodNameResolver(final MethodNameResolver methodNameResolver) {
        this.methodNameResolver = methodNameResolver;
    }

    /**
     * Specify the order value for this HandlerAdapter bean.
     * <p>
     * Default value is <code>Integer.MAX_VALUE</code>, meaning that it's
     * non-ordered.
     *
     * @see org.springframework.core.Ordered#getOrder()
     */
    public void setOrder(final int order) {
        this.order = order;
    }

    /**
     * Sets the parameter name that can be used to determine the requested media
     * type if the {@link #setFavorParameter(boolean)} property is {@code true}.
     * The default parameter name is {@code format}.
     */
    public void setParameterName(final String parameterName) {
        this.parameterName = parameterName;
    }

    /**
     * Set the PathMatcher implementation to use for matching URL paths against
     * registered URL patterns.
     * <p>
     * Default is {@link org.springframework.util.AntPathMatcher}.
     */
    public void setPathMatcher(final PathMatcher pathMatcher) {
        Assert.notNull(pathMatcher, "PathMatcher must not be null");
        this.pathMatcher = pathMatcher;
    }

    /**
     * Set if context path and request URI should be URL-decoded. Both are
     * returned <i>undecoded</i> by the Servlet API, in contrast to the servlet
     * path.
     * <p>
     * Uses either the request encoding or the default encoding according to the
     * Servlet spec (ISO-8859-1).
     *
     * @see org.springframework.web.util.UrlPathHelper#setUrlDecode
     */
    public void setUrlDecode(final boolean urlDecode) {
        this.urlPathHelper.setUrlDecode(urlDecode);
    }

    /**
     * Set the UrlPathHelper to use for resolution of lookup paths.
     * <p>
     * Use this to override the default UrlPathHelper with a custom subclass, or
     * to share common UrlPathHelper settings across multiple HandlerMappings and
     * HandlerAdapters.
     */
    public void setUrlPathHelper(final UrlPathHelper urlPathHelper) {
        Assert.notNull(urlPathHelper, "UrlPathHelper must not be null");
        this.urlPathHelper = urlPathHelper;
    }

    @Override
    public boolean supports(final Object handler) {
        final AnnotationHandlerMethodResolver methodResolver = getMethodResolver(handler);
        return methodResolver.hasHandlerMethods();
    }
}