001/* 002 * Copyright 2002-2015 the original author or authors. 003 * 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 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 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 */ 016 017package org.springframework.web.servlet.mvc.annotation; 018 019import java.lang.reflect.Method; 020import java.util.Arrays; 021import java.util.HashMap; 022import java.util.LinkedHashSet; 023import java.util.Map; 024import java.util.Set; 025import javax.servlet.http.HttpServletRequest; 026 027import org.springframework.context.ApplicationContext; 028import org.springframework.core.annotation.AnnotationUtils; 029import org.springframework.stereotype.Controller; 030import org.springframework.util.ReflectionUtils; 031import org.springframework.util.StringUtils; 032import org.springframework.web.HttpRequestMethodNotSupportedException; 033import org.springframework.web.bind.ServletRequestBindingException; 034import org.springframework.web.bind.UnsatisfiedServletRequestParameterException; 035import org.springframework.web.bind.annotation.RequestMapping; 036import org.springframework.web.bind.annotation.RequestMethod; 037import org.springframework.web.servlet.handler.AbstractDetectingUrlHandlerMapping; 038 039/** 040 * Implementation of the {@link org.springframework.web.servlet.HandlerMapping} 041 * interface that maps handlers based on HTTP paths expressed through the 042 * {@link RequestMapping} annotation at the type or method level. 043 * 044 * <p>Registered by default in {@link org.springframework.web.servlet.DispatcherServlet} 045 * on Java 5+. <b>NOTE:</b> If you define custom HandlerMapping beans in your 046 * DispatcherServlet context, you need to add a DefaultAnnotationHandlerMapping bean 047 * explicitly, since custom HandlerMapping beans replace the default mapping strategies. 048 * Defining a DefaultAnnotationHandlerMapping also allows for registering custom 049 * interceptors: 050 * 051 * <pre class="code"> 052 * <bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping"> 053 * <property name="interceptors"> 054 * ... 055 * </property> 056 * </bean></pre> 057 * 058 * Annotated controllers are usually marked with the {@link Controller} stereotype 059 * at the type level. This is not strictly necessary when {@link RequestMapping} is 060 * applied at the type level (since such a handler usually implements the 061 * {@link org.springframework.web.servlet.mvc.Controller} interface). However, 062 * {@link Controller} is required for detecting {@link RequestMapping} annotations 063 * at the method level if {@link RequestMapping} is not present at the type level. 064 * 065 * <p><b>NOTE:</b> Method-level mappings are only allowed to narrow the mapping 066 * expressed at the class level (if any). HTTP paths need to uniquely map onto 067 * specific handler beans, with any given HTTP path only allowed to be mapped 068 * onto one specific handler bean (not spread across multiple handler beans). 069 * It is strongly recommended to co-locate related handler methods into the same bean. 070 * 071 * <p>The {@link AnnotationMethodHandlerAdapter} is responsible for processing 072 * annotated handler methods, as mapped by this HandlerMapping. For 073 * {@link RequestMapping} at the type level, specific HandlerAdapters such as 074 * {@link org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter} apply. 075 * 076 * @author Juergen Hoeller 077 * @author Arjen Poutsma 078 * @since 2.5 079 * @see RequestMapping 080 * @see AnnotationMethodHandlerAdapter 081 * @deprecated as of Spring 3.2, in favor of 082 * {@link org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping RequestMappingHandlerMapping} 083 */ 084@Deprecated 085public class DefaultAnnotationHandlerMapping extends AbstractDetectingUrlHandlerMapping { 086 087 static final String USE_DEFAULT_SUFFIX_PATTERN = DefaultAnnotationHandlerMapping.class.getName() + ".useDefaultSuffixPattern"; 088 089 private boolean useDefaultSuffixPattern = true; 090 091 private final Map<Class<?>, RequestMapping> cachedMappings = new HashMap<Class<?>, RequestMapping>(); 092 093 094 /** 095 * Set whether to register paths using the default suffix pattern as well: 096 * i.e. whether "/users" should be registered as "/users.*" and "/users/" too. 097 * <p>Default is "true". Turn this convention off if you intend to interpret 098 * your {@code @RequestMapping} paths strictly. 099 * <p>Note that paths which include a ".xxx" suffix or end with "/" already will not be 100 * transformed using the default suffix pattern in any case. 101 */ 102 public void setUseDefaultSuffixPattern(boolean useDefaultSuffixPattern) { 103 this.useDefaultSuffixPattern = useDefaultSuffixPattern; 104 } 105 106 107 /** 108 * Checks for presence of the {@link org.springframework.web.bind.annotation.RequestMapping} 109 * annotation on the handler class and on any of its methods. 110 */ 111 @Override 112 protected String[] determineUrlsForHandler(String beanName) { 113 ApplicationContext context = getApplicationContext(); 114 Class<?> handlerType = context.getType(beanName); 115 RequestMapping mapping = context.findAnnotationOnBean(beanName, RequestMapping.class); 116 if (mapping != null) { 117 // @RequestMapping found at type level 118 this.cachedMappings.put(handlerType, mapping); 119 Set<String> urls = new LinkedHashSet<String>(); 120 String[] typeLevelPatterns = mapping.value(); 121 if (typeLevelPatterns.length > 0) { 122 // @RequestMapping specifies paths at type level 123 String[] methodLevelPatterns = determineUrlsForHandlerMethods(handlerType, true); 124 for (String typeLevelPattern : typeLevelPatterns) { 125 if (!typeLevelPattern.startsWith("/")) { 126 typeLevelPattern = "/" + typeLevelPattern; 127 } 128 boolean hasEmptyMethodLevelMappings = false; 129 for (String methodLevelPattern : methodLevelPatterns) { 130 if (methodLevelPattern == null) { 131 hasEmptyMethodLevelMappings = true; 132 } 133 else { 134 String combinedPattern = getPathMatcher().combine(typeLevelPattern, methodLevelPattern); 135 addUrlsForPath(urls, combinedPattern); 136 } 137 } 138 if (hasEmptyMethodLevelMappings || 139 org.springframework.web.servlet.mvc.Controller.class.isAssignableFrom(handlerType)) { 140 addUrlsForPath(urls, typeLevelPattern); 141 } 142 } 143 return StringUtils.toStringArray(urls); 144 } 145 else { 146 // actual paths specified by @RequestMapping at method level 147 return determineUrlsForHandlerMethods(handlerType, false); 148 } 149 } 150 else if (AnnotationUtils.findAnnotation(handlerType, Controller.class) != null) { 151 // @RequestMapping to be introspected at method level 152 return determineUrlsForHandlerMethods(handlerType, false); 153 } 154 else { 155 return null; 156 } 157 } 158 159 /** 160 * Derive URL mappings from the handler's method-level mappings. 161 * @param handlerType the handler type to introspect 162 * @param hasTypeLevelMapping whether the method-level mappings are nested 163 * within a type-level mapping 164 * @return the array of mapped URLs 165 */ 166 protected String[] determineUrlsForHandlerMethods(Class<?> handlerType, final boolean hasTypeLevelMapping) { 167 String[] subclassResult = determineUrlsForHandlerMethods(handlerType); 168 if (subclassResult != null) { 169 return subclassResult; 170 } 171 172 final Set<String> urls = new LinkedHashSet<String>(); 173 Set<Class<?>> handlerTypes = new LinkedHashSet<Class<?>>(); 174 handlerTypes.add(handlerType); 175 handlerTypes.addAll(Arrays.asList(handlerType.getInterfaces())); 176 for (Class<?> currentHandlerType : handlerTypes) { 177 ReflectionUtils.doWithMethods(currentHandlerType, new ReflectionUtils.MethodCallback() { 178 @Override 179 public void doWith(Method method) { 180 RequestMapping mapping = AnnotationUtils.findAnnotation(method, RequestMapping.class); 181 if (mapping != null) { 182 String[] mappedPatterns = mapping.value(); 183 if (mappedPatterns.length > 0) { 184 for (String mappedPattern : mappedPatterns) { 185 if (!hasTypeLevelMapping && !mappedPattern.startsWith("/")) { 186 mappedPattern = "/" + mappedPattern; 187 } 188 addUrlsForPath(urls, mappedPattern); 189 } 190 } 191 else if (hasTypeLevelMapping) { 192 // empty method-level RequestMapping 193 urls.add(null); 194 } 195 } 196 } 197 }, ReflectionUtils.USER_DECLARED_METHODS); 198 } 199 return StringUtils.toStringArray(urls); 200 } 201 202 /** 203 * Derive URL mappings from the handler's method-level mappings. 204 * @param handlerType the handler type to introspect 205 * @return the array of mapped URLs 206 */ 207 protected String[] determineUrlsForHandlerMethods(Class<?> handlerType) { 208 return null; 209 } 210 211 /** 212 * Add URLs and/or URL patterns for the given path. 213 * @param urls the Set of URLs for the current bean 214 * @param path the currently introspected path 215 */ 216 protected void addUrlsForPath(Set<String> urls, String path) { 217 urls.add(path); 218 if (this.useDefaultSuffixPattern && path.indexOf('.') == -1 && !path.endsWith("/")) { 219 urls.add(path + ".*"); 220 urls.add(path + "/"); 221 } 222 } 223 224 225 /** 226 * Validate the given annotated handler against the current request. 227 * @see #validateMapping 228 */ 229 @Override 230 protected void validateHandler(Object handler, HttpServletRequest request) throws Exception { 231 RequestMapping mapping = this.cachedMappings.get(handler.getClass()); 232 if (mapping == null) { 233 mapping = AnnotationUtils.findAnnotation(handler.getClass(), RequestMapping.class); 234 } 235 if (mapping != null) { 236 validateMapping(mapping, request); 237 } 238 request.setAttribute(USE_DEFAULT_SUFFIX_PATTERN, this.useDefaultSuffixPattern); 239 } 240 241 /** 242 * Validate the given type-level mapping metadata against the current request, 243 * checking HTTP request method and parameter conditions. 244 * @param mapping the mapping metadata to validate 245 * @param request current HTTP request 246 * @throws Exception if validation failed 247 */ 248 protected void validateMapping(RequestMapping mapping, HttpServletRequest request) throws Exception { 249 RequestMethod[] mappedMethods = mapping.method(); 250 if (!ServletAnnotationMappingUtils.checkRequestMethod(mappedMethods, request)) { 251 String[] supportedMethods = new String[mappedMethods.length]; 252 for (int i = 0; i < mappedMethods.length; i++) { 253 supportedMethods[i] = mappedMethods[i].name(); 254 } 255 throw new HttpRequestMethodNotSupportedException(request.getMethod(), supportedMethods); 256 } 257 258 String[] mappedParams = mapping.params(); 259 if (!ServletAnnotationMappingUtils.checkParameters(mappedParams, request)) { 260 throw new UnsatisfiedServletRequestParameterException(mappedParams, request.getParameterMap()); 261 } 262 263 String[] mappedHeaders = mapping.headers(); 264 if (!ServletAnnotationMappingUtils.checkHeaders(mappedHeaders, request)) { 265 throw new ServletRequestBindingException("Header conditions \"" + 266 StringUtils.arrayToDelimitedString(mappedHeaders, ", ") + 267 "\" not met for actual request"); 268 } 269 } 270 271 @Override 272 protected boolean supportsTypeLevelMappings() { 273 return true; 274 } 275}