001/*
002 * Copyright 2002-2018 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.method;
018
019import java.lang.reflect.Method;
020import java.util.ArrayList;
021import java.util.Collections;
022import java.util.Comparator;
023import java.util.LinkedHashMap;
024import java.util.LinkedHashSet;
025import java.util.List;
026import java.util.Map;
027import java.util.Set;
028import javax.servlet.ServletException;
029import javax.servlet.http.HttpServletRequest;
030
031import org.springframework.http.HttpHeaders;
032import org.springframework.http.HttpMethod;
033import org.springframework.http.InvalidMediaTypeException;
034import org.springframework.http.MediaType;
035import org.springframework.util.CollectionUtils;
036import org.springframework.util.MultiValueMap;
037import org.springframework.util.StringUtils;
038import org.springframework.web.HttpMediaTypeNotAcceptableException;
039import org.springframework.web.HttpMediaTypeNotSupportedException;
040import org.springframework.web.HttpRequestMethodNotSupportedException;
041import org.springframework.web.bind.UnsatisfiedServletRequestParameterException;
042import org.springframework.web.bind.annotation.RequestMethod;
043import org.springframework.web.method.HandlerMethod;
044import org.springframework.web.servlet.HandlerMapping;
045import org.springframework.web.servlet.handler.AbstractHandlerMethodMapping;
046import org.springframework.web.servlet.mvc.condition.NameValueExpression;
047import org.springframework.web.util.WebUtils;
048
049/**
050 * Abstract base class for classes for which {@link RequestMappingInfo} defines
051 * the mapping between a request and a handler method.
052 *
053 * @author Arjen Poutsma
054 * @author Rossen Stoyanchev
055 * @since 3.1
056 */
057public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMethodMapping<RequestMappingInfo> {
058
059        private static final Method HTTP_OPTIONS_HANDLE_METHOD;
060
061        static {
062                try {
063                        HTTP_OPTIONS_HANDLE_METHOD = HttpOptionsHandler.class.getMethod("handle");
064                }
065                catch (NoSuchMethodException ex) {
066                        // Should never happen
067                        throw new IllegalStateException("Failed to retrieve internal handler method for HTTP OPTIONS", ex);
068                }
069        }
070
071
072        protected RequestMappingInfoHandlerMapping() {
073                setHandlerMethodMappingNamingStrategy(new RequestMappingInfoHandlerMethodMappingNamingStrategy());
074        }
075
076
077        /**
078         * Get the URL path patterns associated with this {@link RequestMappingInfo}.
079         */
080        @Override
081        protected Set<String> getMappingPathPatterns(RequestMappingInfo info) {
082                return info.getPatternsCondition().getPatterns();
083        }
084
085        /**
086         * Check if the given RequestMappingInfo matches the current request and
087         * return a (potentially new) instance with conditions that match the
088         * current request -- for example with a subset of URL patterns.
089         * @return an info in case of a match; or {@code null} otherwise.
090         */
091        @Override
092        protected RequestMappingInfo getMatchingMapping(RequestMappingInfo info, HttpServletRequest request) {
093                return info.getMatchingCondition(request);
094        }
095
096        /**
097         * Provide a Comparator to sort RequestMappingInfos matched to a request.
098         */
099        @Override
100        protected Comparator<RequestMappingInfo> getMappingComparator(final HttpServletRequest request) {
101                return new Comparator<RequestMappingInfo>() {
102                        @Override
103                        public int compare(RequestMappingInfo info1, RequestMappingInfo info2) {
104                                return info1.compareTo(info2, request);
105                        }
106                };
107        }
108
109        /**
110         * Expose URI template variables, matrix variables, and producible media types in the request.
111         * @see HandlerMapping#URI_TEMPLATE_VARIABLES_ATTRIBUTE
112         * @see HandlerMapping#MATRIX_VARIABLES_ATTRIBUTE
113         * @see HandlerMapping#PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE
114         */
115        @Override
116        protected void handleMatch(RequestMappingInfo info, String lookupPath, HttpServletRequest request) {
117                super.handleMatch(info, lookupPath, request);
118
119                String bestPattern;
120                Map<String, String> uriVariables;
121                Map<String, String> decodedUriVariables;
122
123                Set<String> patterns = info.getPatternsCondition().getPatterns();
124                if (patterns.isEmpty()) {
125                        bestPattern = lookupPath;
126                        uriVariables = Collections.emptyMap();
127                        decodedUriVariables = Collections.emptyMap();
128                }
129                else {
130                        bestPattern = patterns.iterator().next();
131                        uriVariables = getPathMatcher().extractUriTemplateVariables(bestPattern, lookupPath);
132                        decodedUriVariables = getUrlPathHelper().decodePathVariables(request, uriVariables);
133                }
134
135                request.setAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE, bestPattern);
136                request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, decodedUriVariables);
137
138                if (isMatrixVariableContentAvailable()) {
139                        Map<String, MultiValueMap<String, String>> matrixVars = extractMatrixVariables(request, uriVariables);
140                        request.setAttribute(HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE, matrixVars);
141                }
142
143                if (!info.getProducesCondition().getProducibleMediaTypes().isEmpty()) {
144                        Set<MediaType> mediaTypes = info.getProducesCondition().getProducibleMediaTypes();
145                        request.setAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, mediaTypes);
146                }
147        }
148
149        private boolean isMatrixVariableContentAvailable() {
150                return !getUrlPathHelper().shouldRemoveSemicolonContent();
151        }
152
153        private Map<String, MultiValueMap<String, String>> extractMatrixVariables(
154                        HttpServletRequest request, Map<String, String> uriVariables) {
155
156                Map<String, MultiValueMap<String, String>> result = new LinkedHashMap<String, MultiValueMap<String, String>>();
157                for (Map.Entry<String, String> uriVar : uriVariables.entrySet()) {
158                        String uriVarValue = uriVar.getValue();
159
160                        int equalsIndex = uriVarValue.indexOf('=');
161                        if (equalsIndex == -1) {
162                                continue;
163                        }
164
165                        String matrixVariables;
166
167                        int semicolonIndex = uriVarValue.indexOf(';');
168                        if ((semicolonIndex == -1) || (semicolonIndex == 0) || (equalsIndex < semicolonIndex)) {
169                                matrixVariables = uriVarValue;
170                        }
171                        else {
172                                matrixVariables = uriVarValue.substring(semicolonIndex + 1);
173                                uriVariables.put(uriVar.getKey(), uriVarValue.substring(0, semicolonIndex));
174                        }
175
176                        MultiValueMap<String, String> vars = WebUtils.parseMatrixVariables(matrixVariables);
177                        result.put(uriVar.getKey(), getUrlPathHelper().decodeMatrixVariables(request, vars));
178                }
179                return result;
180        }
181
182        /**
183         * Iterate all RequestMappingInfo's once again, look if any match by URL at
184         * least and raise exceptions according to what doesn't match.
185         * @throws HttpRequestMethodNotSupportedException if there are matches by URL
186         * but not by HTTP method
187         * @throws HttpMediaTypeNotAcceptableException if there are matches by URL
188         * but not by consumable/producible media types
189         */
190        @Override
191        protected HandlerMethod handleNoMatch(
192                        Set<RequestMappingInfo> infos, String lookupPath, HttpServletRequest request) throws ServletException {
193
194                PartialMatchHelper helper = new PartialMatchHelper(infos, request);
195                if (helper.isEmpty()) {
196                        return null;
197                }
198
199                if (helper.hasMethodsMismatch()) {
200                        Set<String> methods = helper.getAllowedMethods();
201                        if (HttpMethod.OPTIONS.matches(request.getMethod())) {
202                                HttpOptionsHandler handler = new HttpOptionsHandler(methods);
203                                return new HandlerMethod(handler, HTTP_OPTIONS_HANDLE_METHOD);
204                        }
205                        throw new HttpRequestMethodNotSupportedException(request.getMethod(), methods);
206                }
207
208                if (helper.hasConsumesMismatch()) {
209                        Set<MediaType> mediaTypes = helper.getConsumableMediaTypes();
210                        MediaType contentType = null;
211                        if (StringUtils.hasLength(request.getContentType())) {
212                                try {
213                                        contentType = MediaType.parseMediaType(request.getContentType());
214                                }
215                                catch (InvalidMediaTypeException ex) {
216                                        throw new HttpMediaTypeNotSupportedException(ex.getMessage());
217                                }
218                        }
219                        throw new HttpMediaTypeNotSupportedException(contentType, new ArrayList<MediaType>(mediaTypes));
220                }
221
222                if (helper.hasProducesMismatch()) {
223                        Set<MediaType> mediaTypes = helper.getProducibleMediaTypes();
224                        throw new HttpMediaTypeNotAcceptableException(new ArrayList<MediaType>(mediaTypes));
225                }
226
227                if (helper.hasParamsMismatch()) {
228                        List<String[]> conditions = helper.getParamConditions();
229                        throw new UnsatisfiedServletRequestParameterException(conditions, request.getParameterMap());
230                }
231
232                return null;
233        }
234
235
236        /**
237         * Aggregate all partial matches and expose methods checking across them.
238         */
239        private static class PartialMatchHelper {
240
241                private final List<PartialMatch> partialMatches = new ArrayList<PartialMatch>();
242
243                public PartialMatchHelper(Set<RequestMappingInfo> infos, HttpServletRequest request) {
244                        for (RequestMappingInfo info : infos) {
245                                if (info.getPatternsCondition().getMatchingCondition(request) != null) {
246                                        this.partialMatches.add(new PartialMatch(info, request));
247                                }
248                        }
249                }
250
251                /**
252                 * Whether there any partial matches.
253                 */
254                public boolean isEmpty() {
255                        return this.partialMatches.isEmpty();
256                }
257
258                /**
259                 * Any partial matches for "methods"?
260                 */
261                public boolean hasMethodsMismatch() {
262                        for (PartialMatch match : this.partialMatches) {
263                                if (match.hasMethodsMatch()) {
264                                        return false;
265                                }
266                        }
267                        return true;
268                }
269
270                /**
271                 * Any partial matches for "methods" and "consumes"?
272                 */
273                public boolean hasConsumesMismatch() {
274                        for (PartialMatch match : this.partialMatches) {
275                                if (match.hasConsumesMatch()) {
276                                        return false;
277                                }
278                        }
279                        return true;
280                }
281
282                /**
283                 * Any partial matches for "methods", "consumes", and "produces"?
284                 */
285                public boolean hasProducesMismatch() {
286                        for (PartialMatch match : this.partialMatches) {
287                                if (match.hasProducesMatch()) {
288                                        return false;
289                                }
290                        }
291                        return true;
292                }
293
294                /**
295                 * Any partial matches for "methods", "consumes", "produces", and "params"?
296                 */
297                public boolean hasParamsMismatch() {
298                        for (PartialMatch match : this.partialMatches) {
299                                if (match.hasParamsMatch()) {
300                                        return false;
301                                }
302                        }
303                        return true;
304                }
305
306        ="sourceLineNo">307                 * Return declared HTTP methods.
308                 */
309                public Set<String> getAllowedMethods() {
310                        Set<String> result = new LinkedHashSet<String>();
311                        for (PartialMatch match : this.partialMatches) {
312                                for (RequestMethod method : match.getInfo().getMethodsCondition().getMethods()) {
313                                        result.add(method.name());
314                                }
315                        }
316                        return result;
317                }
318
319                /**
320                 * Return declared "consumable" types but only among those that also
321                 * match the "methods" condition.
322                 */
323                public Set<MediaType> getConsumableMediaTypes() {
324                        Set<MediaType> result = new LinkedHashSet<MediaType>();
325                        for (PartialMatch match : this.partialMatches) {
326                                if (match.hasMethodsMatch()) {
327                                        result.addAll(match.getInfo().getConsumesCondition().getConsumableMediaTypes());
328                                }
329                        }
330                        return result;
331                }
332
333                /**
334                 * Return declared "producible" types but only among those that also
335                 * match the "methods" and "consumes" conditions.
336                 */
337                public Set<MediaType> getProducibleMediaTypes() {
338                        Set<MediaType> result = new LinkedHashSet<MediaType>();
339                        for (PartialMatch match : this.partialMatches) {
340                                if (match.hasConsumesMatch()) {
341                                        result.addAll(match.getInfo().getProducesCondition().getProducibleMediaTypes());
342                                }
343                        }
344                        return result;
345                }
346
347                /**
348                 * Return declared "params" conditions but only among those that also
349                 * match the "methods", "consumes", and "params" conditions.
350                 */
351                public List<String[]> getParamConditions() {
352                        List<String[]> result = new ArrayList<String[]>();
353                        for (PartialMatch match : this.partialMatches) {
354                                if (match.hasProducesMatch()) {
355                                        Set<NameValueExpression<String>> set = match.getInfo().getParamsCondition().getExpressions();
356                                        if (!CollectionUtils.isEmpty(set)) {
357                                                int i = 0;
358                                                String[] array = new String[set.size()];
359                                                for (NameValueExpression<String> expression : set) {
360                                                        array[i++] = expression.toString();
361                                                }
362                                                result.add(array);
363                                        }
364                                }
365                        }
366                        return result;
367                }
368
369
370                /**
371                 * Container for a RequestMappingInfo that matches the URL path at least.
372                 */
373                private static class PartialMatch {
374
375                        private final RequestMappingInfo info;
376
377                        private final boolean methodsMatch;
378
379                        private final boolean consumesMatch;
380
381                        private final boolean producesMatch;
382
383                        private final boolean paramsMatch;
384
385                        /**
386                         * @param info RequestMappingInfo that matches the URL path.
387                         * @param request the current request
388                         */
389                        public PartialMatch(RequestMappingInfo info, HttpServletRequest request) {
390                                this.info = info;
391                                this.methodsMatch = (info.getMethodsCondition().getMatchingCondition(request) != null);
392                                this.consumesMatch = (info.getConsumesCondition().getMatchingCondition(request) != null);
393                                this.producesMatch = (info.getProducesCondition().getMatchingCondition(request) != null);
394                                this.paramsMatch = (info.getParamsCondition().getMatchingCondition(request) != null);
395                        }
396
397                        public RequestMappingInfo getInfo() {
398                                return this.info;
399                        }
400
401                        public boolean hasMethodsMatch() {
402                                return this.methodsMatch;
403                        }
404
405                        public boolean hasConsumesMatch() {
406                                return (hasMethodsMatch() && this.consumesMatch);
407                        }
408
409                        public boolean hasProducesMatch() {
410                                return (hasConsumesMatch() && this.producesMatch);
411                        }
412
413                        public boolean hasParamsMatch() {
414                                return (hasProducesMatch() && this.paramsMatch);
415                        }
416
417                        @Override
418                        public String toString() {
419                                return this.info.toString();
420                        }
421                }
422        }
423
424
425        /**
426         * Default handler for HTTP OPTIONS.
427         */
428        private static class HttpOptionsHandler {
429
430                private final HttpHeaders headers = new HttpHeaders();
431
432                public HttpOptionsHandler(Set<String> declaredMethods) {
433                        this.headers.setAllow(initAllowedHttpMethods(declaredMethods));
434                }
435
436                private static Set<HttpMethod> initAllowedHttpMethods(Set<String> declaredMethods) {
437                        Set<HttpMethod> result = new LinkedHashSet<HttpMethod>(declaredMethods.size());
438                        if (declaredMethods.isEmpty()) {
439                                for (HttpMethod method : HttpMethod.values()) {
440                                        if (method != HttpMethod.TRACE) {
441                                                result.add(method);
442                                        }
443                                }
444                        }
445                        else {
446                                for (String method : declaredMethods) {
447                                        HttpMethod httpMethod = HttpMethod.valueOf(method);
448                                        result.add(httpMethod);
449                                        if (httpMethod == HttpMethod.GET) {
450                                                result.add(HttpMethod.HEAD);
451                                        }
452                                }
453                        }
454                        return result;
455                }
456
457                public HttpHeaders handle() {
458                        return this.headers;
459                }
460        }
461
462}