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 * &lt;bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping"&gt;
053 *   &lt;property name="interceptors"&gt;
054 *     ...
055 *   &lt;/property&gt;
056 * &lt;/bean&gt;</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}