/* * 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(); } }