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