001/*
002 * Copyright 2002-2017 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.util.List;
020import javax.servlet.http.HttpServletRequest;
021
022import org.springframework.http.HttpMethod;
023import org.springframework.util.PathMatcher;
024import org.springframework.util.StringUtils;
025import org.springframework.web.accept.ContentNegotiationManager;
026import org.springframework.web.bind.annotation.RequestMethod;
027import org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition;
028import org.springframework.web.servlet.mvc.condition.HeadersRequestCondition;
029import org.springframework.web.servlet.mvc.condition.ParamsRequestCondition;
030import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
031import org.springframework.web.servlet.mvc.condition.ProducesRequestCondition;
032import org.springframework.web.servlet.mvc.condition.RequestCondition;
033import org.springframework.web.servlet.mvc.condition.RequestConditionHolder;
034import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
035import org.springframework.web.util.UrlPathHelper;
036
037/**
038 * A {@link RequestCondition} that consists of the following other conditions:
039 * <ol>
040 * <li>{@link PatternsRequestCondition}
041 * <li>{@link RequestMethodsRequestCondition}
042 * <li>{@link ParamsRequestCondition}
043 * <li>{@link HeadersRequestCondition}
044 * <li>{@link ConsumesRequestCondition}
045 * <li>{@link ProducesRequestCondition}
046 * <li>{@code RequestCondition} (optional, custom request condition)
047 * </ol>
048 *
049 * @author Arjen Poutsma
050 * @author Rossen Stoyanchev
051 * @since 3.1
052 */
053public final class RequestMappingInfo implements RequestCondition<RequestMappingInfo> {
054
055        private final String name;
056
057        private final PatternsRequestCondition patternsCondition;
058
059        private final RequestMethodsRequestCondition methodsCondition;
060
061        private final ParamsRequestCondition paramsCondition;
062
063        private final HeadersRequestCondition headersCondition;
064
065        private final ConsumesRequestCondition consumesCondition;
066
067        private final ProducesRequestCondition producesCondition;
068
069        private final RequestConditionHolder customConditionHolder;
070
071
072        public RequestMappingInfo(String name, PatternsRequestCondition patterns, RequestMethodsRequestCondition methods,
073                        ParamsRequestCondition params, HeadersRequestCondition headers, ConsumesRequestCondition consumes,
074                        ProducesRequestCondition produces, RequestCondition<?> custom) {
075
076                this.name = (StringUtils.hasText(name) ? name : null);
077                this.patternsCondition = (patterns != null ? patterns : new PatternsRequestCondition());
078                this.methodsCondition = (methods != null ? methods : new RequestMethodsRequestCondition());
079                this.paramsCondition = (params != null ? params : new ParamsRequestCondition());
080                this.headersCondition = (headers != null ? headers : new HeadersRequestCondition());
081                this.consumesCondition = (consumes != null ? consumes : new ConsumesRequestCondition());
082                this.producesCondition = (produces != null ? produces : new ProducesRequestCondition());
083                this.customConditionHolder = new RequestConditionHolder(custom);
084        }
085
086        /**
087         * Creates a new instance with the given request conditions.
088         */
089        public RequestMappingInfo(PatternsRequestCondition patterns, RequestMethodsRequestCondition methods,
090                        ParamsRequestCondition params, HeadersRequestCondition headers, ConsumesRequestCondition consumes,
091                        ProducesRequestCondition produces, RequestCondition<?> custom) {
092
093                this(null, patterns, methods, params, headers, consumes, produces, custom);
094        }
095
096        /**
097         * Re-create a RequestMappingInfo with the given custom request condition.
098         */
099        public RequestMappingInfo(RequestMappingInfo info, RequestCondition<?> customRequestCondition) {
100                this(info.name, info.patternsCondition, info.methodsCondition, info.paramsCondition, info.headersCondition,
101                                info.consumesCondition, info.producesCondition, customRequestCondition);
102        }
103
104
105        /**
106         * Return the name for this mapping, or {@code null}.
107         */
108        public String getName() {
109                return this.name;
110        }
111
112        /**
113         * Return the URL patterns of this {@link RequestMappingInfo};
114         * or instance with 0 patterns (never {@code null}).
115         */
116        public PatternsRequestCondition getPatternsCondition() {
117                return this.patternsCondition;
118        }
119
120        /**
121         * Return the HTTP request methods of this {@link RequestMappingInfo};
122         * or instance with 0 request methods (never {@code null}).
123         */
124        public RequestMethodsRequestCondition getMethodsCondition() {
125                return this.methodsCondition;
126        }
127
128        /**
129         * Return the "parameters" condition of this {@link RequestMappingInfo};
130         * or instance with 0 parameter expressions (never {@code null}).
131         */
132        public ParamsRequestCondition getParamsCondition() {
133                return this.paramsCondition;
134        }
135
136        /**
137         * Return the "headers" condition of this {@link RequestMappingInfo};
138         * or instance with 0 header expressions (never {@code null}).
139         */
140        public HeadersRequestCondition getHeadersCondition() {
141                return this.headersCondition;
142        }
143
144        /**
145         * Return the "consumes" condition of this {@link RequestMappingInfo};
146         * or instance with 0 consumes expressions (never {@code null}).
147         */
148        public ConsumesRequestCondition getConsumesCondition() {
149                return this.consumesCondition;
150        }
151
152        /**
153         * Return the "produces" condition of this {@link RequestMappingInfo};
154         * or instance with 0 produces expressions (never {@code null}).
155         */
156        public ProducesRequestCondition getProducesCondition() {
157                return this.producesCondition;
158        }
159
160        /**
161         * Return the "custom" condition of this {@link RequestMappingInfo}, or {@code null}.
162         */
163        public RequestCondition<?> getCustomCondition() {
164                return this.customConditionHolder.getCondition();
165        }
166
167
168        /**
169         * Combine "this" request mapping info (i.e. the current instance) with another request mapping info instance.
170         * <p>Example: combine type- and method-level request mappings.
171         * @return a new request mapping info instance; never {@code null}
172         */
173        @Override
174        public RequestMappingInfo combine(RequestMappingInfo other) {
175                String name = combineNames(other);
176                PatternsRequestCondition patterns = this.patternsCondition.combine(other.patternsCondition);
177                RequestMethodsRequestCondition methods = this.methodsCondition.combine(other.methodsCondition);
178                ParamsRequestCondition params = this.paramsCondition.combine(other.paramsCondition);
179                HeadersRequestCondition headers = this.headersCondition.combine(other.headersCondition);
180                ConsumesRequestCondition consumes = this.consumesCondition.combine(other.consumesCondition);
181                ProducesRequestCondition produces = this.producesCondition.combine(other.producesCondition);
182                RequestConditionHolder custom = this.customConditionHolder.combine(other.customConditionHolder);
183
184                return new RequestMappingInfo(name, patterns,
185                                methods, params, headers, consumes, produces, custom.getCondition());
186        }
187
188        private String combineNames(RequestMappingInfo other) {
189                if (this.name != null && other.name != null) {
190                        String separator = RequestMappingInfoHandlerMethodMappingNamingStrategy.SEPARATOR;
191                        return this.name + separator + other.name;
192                }
193                else if (this.name != null) {
194                        return this.name;
195                }
196                else {
197                        return other.name;
198                }
199        }
200
201        /**
202         * Checks if all conditions in this request mapping info match the provided request and returns
203         * a potentially new request mapping info with conditions tailored to the current request.
204         * <p>For example the returned instance may contain the subset of URL patterns that match to
205         * the current request, sorted with best matching patterns on top.
206         * @return a new instance in case all conditions match; or {@code null} otherwise
207         */
208        @Override
209        public RequestMappingInfo getMatchingCondition(HttpServletRequest request) {
210                RequestMethodsRequestCondition methods = this.methodsCondition.getMatchingCondition(request);
211                ParamsRequestCondition params = this.paramsCondition.getMatchingCondition(request);
212                HeadersRequestCondition headers = this.headersCondition.getMatchingCondition(request);
213                ConsumesRequestCondition consumes = this.consumesCondition.getMatchingCondition(request);
214                ProducesRequestCondition produces = this.producesCondition.getMatchingCondition(request);
215
216                if (methods == null || params == null || headers == null || consumes == null || produces == null) {
217                        return null;
218                }
219
220                PatternsRequestCondition patterns = this.patternsCondition.getMatchingCondition(request);
221                if (patterns == null) {
222                        return null;
223                }
224
225                RequestConditionHolder custom = this.customConditionHolder.getMatchingCondition(request);
226                if (custom == null) {
227                        return null;
228                }
229
230                return new RequestMappingInfo(this.name, patterns,
231                                methods, params, headers, consumes, produces, custom.getCondition());
232        }
233
234        /**
235         * Compares "this" info (i.e. the current instance) with another info in the context of a request.
236         * <p>Note: It is assumed both instances have been obtained via
237         * {@link #getMatchingCondition(HttpServletRequest)} to ensure they have conditions with
238         * content relevant to current request.
239         */
240        @Override
241        public int compareTo(RequestMappingInfo other, HttpServletRequest request) {
242                int result;
243                // Automatic vs explicit HTTP HEAD mapping
244                if (HttpMethod.HEAD.matches(request.getMethod())) {
245                        result = this.methodsCondition.compareTo(other.getMethodsCondition(), request);
246                        if (result != 0) {
247                                return result;
248                        }
249                }
250                result = this.patternsCondition.compareTo(other.getPatternsCondition(), request);
251                if (result != 0) {
252                        return result;
253                }
254                result = this.paramsCondition.compareTo(other.getParamsCondition(), request);
255                if (result != 0) {
256                        return result;
257                }
258                result = this.headersCondition.compareTo(other.getHeadersCondition(), request);
259                if (result != 0) {
260                        return result;
261                }
262                result = this.consumesCondition.compareTo(other.getConsumesCondition(), request);
263                if (result != 0) {
264                        return result;
265                }
266                result = this.producesCondition.compareTo(other.getProducesCondition(), request);
267                if (result != 0) {
268                        return result;
269                }
270                // Implicit (no method) vs explicit HTTP method mappings
271                result = this.methodsCondition.compareTo(other.getMethodsCondition(), request);
272                if (result != 0) {
273                        return result;
274                }
275                result = this.customConditionHolder.compareTo(other.customConditionHolder, request);
276                if (result != 0) {
277                        return result;
278                }
279                return 0;
280        }
281
282        @Override
283        public boolean equals(Object other) {
284                if (this == other) {
285                        return true;
286                }
287                if (!(other instanceof RequestMappingInfo)) {
288                        return false;
289                }
290                RequestMappingInfo otherInfo = (RequestMappingInfo) other;
291                return (this.patternsCondition.equals(otherInfo.patternsCondition) &&
292                                this.methodsCondition.equals(otherInfo.methodsCondition) &&
293                                this.paramsCondition.equals(otherInfo.paramsCondition) &&
294                                this.headersCondition.equals(otherInfo.headersCondition) &&
295                                this.consumesCondition.equals(otherInfo.consumesCondition) &&
296                                this.producesCondition.equals(otherInfo.producesCondition) &&
297                                this.customConditionHolder.equals(otherInfo.customConditionHolder));
298        }
299
300<   @Override
301        public int hashCode() {
302                return (this.patternsCondition.hashCode() * 31 +  // primary differentiation
303                                this.methodsCondition.hashCode() + this.paramsCondition.hashCode() +
304                                this.headersCondition.hashCode() + this.consumesCondition.hashCode() +
305                                this.producesCondition.hashCode() + this.customConditionHolder.hashCode());
306        }
307
308        @Override
309        public String toString() {
310                StringBuilder builder = new StringBuilder("{");
311                builder.append(this.patternsCondition);
312                if (!this.methodsCondition.isEmpty()) {
313                        builder.append(",methods=").append(this.methodsCondition);
314                }
315                if (!this.paramsCondition.isEmpty()) {
316                        builder.append(",params=").append(this.paramsCondition);
317                }
318                if (!this.headersCondition.isEmpty()) {
319                        builder.append(",headers=").append(this.headersCondition);
320                }
321                if (!this.consumesCondition.isEmpty()) {
322                        builder.append(",consumes=").append(this.consumesCondition);
323                }
324                if (!this.producesCondition.isEmpty()) {
325                        builder.append(",produces=").append(this.producesCondition);
326                }
327                if (!this.customConditionHolder.isEmpty()) {
328                        builder.append(",custom=").append(this.customConditionHolder);
329                }
330                builder.append('}');
331                return builder.toString();
332        }
333
334
335        /**
336         * Create a new {@code RequestMappingInfo.Builder} with the given paths.
337         * @param paths the paths to use
338         * @since 4.2
339         */
340        public static Builder paths(String... paths) {
341                return new DefaultBuilder(paths);
342        }
343
344
345        /**
346         * Defines a builder for creating a RequestMappingInfo.
347         * @since 4.2
348         */
349        public interface Builder {
350
351                /**
352                 * Set the path patterns.
353                 */
354                Builder paths(String... paths);
355
356                /**
357                 * Set the request method conditions.
358                 */
359                Builder methods(RequestMethod... methods);
360
361                /**
362                 * Set the request param conditions.
363                 */
364                Builder params(String... params);
365
366                /**
367                 * Set the header conditions.
368                 * <p>By default this is not set.
369                 */
370                Builder headers(String... headers);
371
372                /**
373                 * Set the consumes conditions.
374                 */
375                Builder consumes(String... consumes);
376
377                /**
378                 * Set the produces conditions.
379                 */
380                Builder produces(String... produces);
381
382                /**
383                 * Set the mapping name.
384                 */
385                Builder mappingName(String name);
386
387                /**
388                 * Set a custom condition to use.
389                 */
390                Builder customCondition(RequestCondition<?> condition);
391
392                /**
393                 * Provide additional configuration needed for request mapping purposes.
394                 */
395                Builder options(BuilderConfiguration options);
396
397                /**
398                 * Build the RequestMappingInfo.
399                 */
400                RequestMappingInfo build();
401        }
402
403
404        private static class DefaultBuilder implements Builder {
405
406                private String[] paths;
407
408                private RequestMethod[] methods;
409
410                private String[] params;
411
412                private String[] headers;
413
414                private String[] consumes;
415
416                private String[] produces;
417
418                private String mappingName;
419
420                private RequestCondition<?> customCondition;
421
422                private BuilderConfiguration options = new BuilderConfiguration();
423
424                public DefaultBuilder(String... paths) {
425                        this.paths = paths;
426                }
427
428                @Override
429                public Builder paths(String... paths) {
430                        this.paths = paths;
431                        return this;
432                }
433
434                @Override
435                public DefaultBuilder methods(RequestMethod... methods) {
436                        this.methods = methods;
437                        return this;
438                }
439
440                @Override
441                public DefaultBuilder params(String... params) {
442                        this.params = params;
443                        return this;
444                }
445
446                @Override
447                public DefaultBuilder headers(String... headers) {
448                        this.headers = headers;
449                        return this;
450                }
451
452                @Override
453                public DefaultBuilder consumes(String... consumes) {
454                        this.consumes = consumes;
455                        return this;
456                }
457
458                @Override
459                public DefaultBuilder produces(String... produces) {
460                        this.produces = produces;
461                        return this;
462                }
463
464                @Override
465                public DefaultBuilder mappingName(String name) {
466                        this.mappingName = name;
467                        return this;
468                }
469
470                @Override
471                public DefaultBuilder customCondition(RequestCondition<?> condition) {
472                        this.customCondition = condition;
473                        return this;
474                }
475
476                @Override
477                public Builder options(BuilderConfiguration options) {
478                        this.options = options;
479                        return this;
480                }
481
482                @Override
483                public RequestMappingInfo build() {
484                        ContentNegotiationManager manager = this.options.getContentNegotiationManager();
485
486                        PatternsRequestCondition patternsCondition = new PatternsRequestCondition(
487                                        this.paths, this.options.getUrlPathHelper(), this.options.getPathMatcher(),
488                                        this.options.useSuffixPatternMatch(), this.options.useTrailingSlashMatch(),
489                                        this.options.getFileExtensions());
490
491                        return new RequestMappingInfo(this.mappingName, patternsCondition,
492                                        new RequestMethodsRequestCondition(methods),
493                                        new ParamsRequestCondition(this.params),
494                                        new HeadersRequestCondition(this.headers),
495                                        new ConsumesRequestCondition(this.consumes, this.headers),
496                                        new ProducesRequestCondition(this.produces, this.headers, manager),
497                                        this.customCondition);
498                }
499        }
500
501
502        /**
503         * Container for configuration options used for request mapping purposes.
504         * Such configuration is required to create RequestMappingInfo instances but
505         * is typically used across all RequestMappingInfo instances.
506         * @since 4.2
507         * @see Builder#options
508         */
509        public static class BuilderConfiguration {
510
511                private UrlPathHelper urlPathHelper;
512
513                private PathMatcher pathMatcher;
514
515                private boolean trailingSlashMatch = true;
516
517                private boolean suffixPatternMatch = true;
518
519                private boolean registeredSuffixPatternMatch = false;
520
521                private ContentNegotiationManager contentNegotiationManager;
522
523                /**
524                 * @deprecated as of Spring 4.2.8, in favor of {@link #setUrlPathHelper}
525                 */
526                @Deprecated
527                public void setPathHelper(UrlPathHelper pathHelper) {
528                        this.urlPathHelper = pathHelper;
529                }
530
531                /**
532                 * Set a custom UrlPathHelper to use for the PatternsRequestCondition.
533                 * <p>By default this is not set.
534                 * @since 4.2.8
535                 */
536                public void setUrlPathHelper(UrlPathHelper urlPathHelper) {
537                        this.urlPathHelper = urlPathHelper;
538                }
539
540                /**
541                 * Return a custom UrlPathHelper to use for the PatternsRequestCondition, if any.
542                 */
543                public UrlPathHelper getUrlPathHelper() {
544                        return this.urlPathHelper;
545                }
546
547                /**
548                 * Set a custom PathMatcher to use for the PatternsRequestCondition.
549                 * <p>By default this is not set.
550                 */
551                public void setPathMatcher(PathMatcher pathMatcher) {
552                        this.pathMatcher = pathMatcher;
553                }
554
555                /**
556                 * Return a custom PathMatcher to use for the PatternsRequestCondition, if any.
557                 */
558                public PathMatcher getPathMatcher() {
559                        return this.pathMatcher;
560                }
561
562                /**
563                 * Set whether to apply trailing slash matching in PatternsRequestCondition.
564                 * <p>By default this is set to 'true'.
565                 */
566                public void setTrailingSlashMatch(boolean trailingSlashMatch) {
567                        this.trailingSlashMatch = trailingSlashMatch;
568                }
569
570                /**
571                 * Return whether to apply trailing slash matching in PatternsRequestCondition.
572                 */
573                public boolean useTrailingSlashMatch() {
574                        return this.trailingSlashMatch;
575                }
576
577                /**
578                 * Set whether to apply suffix pattern matching in PatternsRequestCondition.
579                 * <p>By default this is set to 'true'.
580                 * @see #setRegisteredSuffixPatternMatch(boolean)
581                 */
582                public void setSuffixPatternMatch(boolean suffixPatternMatch) {
583                        this.suffixPatternMatch = suffixPatternMatch;
584                }
585
586                /**
587                 * Return whether to apply suffix pattern matching in PatternsRequestCondition.
588                 */
589                public boolean useSuffixPatternMatch() {
590                        return this.suffixPatternMatch;
591                }
592
593                /**
594                 * Set whether suffix pattern matching should be restricted to registered
595                 * file extensions only. Setting this property also sets
596                 * {@code suffixPatternMatch=true} and requires that a
597                 * {@link #setContentNegotiationManager} is also configured in order to
598                 * obtain the registered file extensions.
599                 */
600                public void setRegisteredSuffixPatternMatch(boolean registeredSuffixPatternMatch) {
601                        this.registeredSuffixPatternMatch = registeredSuffixPatternMatch;
602                        this.suffixPatternMatch = (registeredSuffixPatternMatch || this.suffixPatternMatch);
603                }
604
605                /**
606                 * Return whether suffix pattern matching should be restricted to registered
607                 * file extensions only.
608                 */
609                public boolean useRegisteredSuffixPatternMatch() {
610                        return this.registeredSuffixPatternMatch;
611                }
612
613                /**
614                 * Return the file extensions to use for suffix pattern matching. If
615                 * {@code registeredSuffixPatternMatch=true}, the extensions are obtained
616                 * from the configured {@code contentNegotiationManager}.
617                 */
618                public List<String> getFileExtensions() {
619                        if (useRegisteredSuffixPatternMatch() && this.contentNegotiationManager != null) {
620                                return this.contentNegotiationManager.getAllFileExtensions();
621                        }
622                        return null;
623                }
624
625                /**
626                 * Set the ContentNegotiationManager to use for the ProducesRequestCondition.
627                 * <p>By default this is not set.
628                 */
629                public void setContentNegotiationManager(ContentNegotiationManager contentNegotiationManager) {
630                        this.contentNegotiationManager = contentNegotiationManager;
631                }
632
633                /**
634                 * Return the ContentNegotiationManager to use for the ProducesRequestCondition,
635                 * if any.
636                 */
637                public ContentNegotiationManager getContentNegotiationManager() {
638                        return this.contentNegotiationManager;
639                }
640        }
641
642}