RequestMappingInfo.java 20.4 KB
Newer Older
1
/*
2
 * Copyright 2002-2017 the original author or authors.
3 4 5 6 7 8 9 10 11 12 13 14 15 16
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

17
package org.springframework.web.servlet.mvc.method;
18

19
import java.util.List;
20 21
import javax.servlet.http.HttpServletRequest;

22
import org.springframework.http.HttpMethod;
23
import org.springframework.lang.Nullable;
24
import org.springframework.util.PathMatcher;
25
import org.springframework.util.StringUtils;
26
import org.springframework.web.accept.ContentNegotiationManager;
S
Sebastien Deleuze 已提交
27
import org.springframework.web.bind.annotation.RequestMethod;
28 29 30 31 32 33
import org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition;
import org.springframework.web.servlet.mvc.condition.HeadersRequestCondition;
import org.springframework.web.servlet.mvc.condition.ParamsRequestCondition;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
import org.springframework.web.servlet.mvc.condition.ProducesRequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
34
import org.springframework.web.servlet.mvc.condition.RequestConditionHolder;
35
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
36
import org.springframework.web.util.UrlPathHelper;
37 38

/**
39
 * A {@link RequestCondition} that consists of the following other conditions:
40
 * <ol>
J
Juergen Hoeller 已提交
41 42 43 44 45 46 47
 * <li>{@link PatternsRequestCondition}
 * <li>{@link RequestMethodsRequestCondition}
 * <li>{@link ParamsRequestCondition}
 * <li>{@link HeadersRequestCondition}
 * <li>{@link ConsumesRequestCondition}
 * <li>{@link ProducesRequestCondition}
 * <li>{@code RequestCondition} (optional, custom request condition)
48
 * </ol>
49
 *
50 51
 * @author Arjen Poutsma
 * @author Rossen Stoyanchev
R
Rossen Stoyanchev 已提交
52
 * @since 3.1
53
 */
54
public final class RequestMappingInfo implements RequestCondition<RequestMappingInfo> {
55

56
	@Nullable
57 58
	private final String name;

59
	private final PatternsRequestCondition patternsCondition;
60

61
	private final RequestMethodsRequestCondition methodsCondition;
62

63
	private final ParamsRequestCondition paramsCondition;
64

65
	private final HeadersRequestCondition headersCondition;
66

67
	private final ConsumesRequestCondition consumesCondition;
68

69
	private final ProducesRequestCondition producesCondition;
70

71
	private final RequestConditionHolder customConditionHolder;
72

73

74 75 76
	public RequestMappingInfo(@Nullable String name, @Nullable PatternsRequestCondition patterns, @Nullable RequestMethodsRequestCondition methods,
			@Nullable ParamsRequestCondition params, @Nullable HeadersRequestCondition headers, @Nullable ConsumesRequestCondition consumes,
			@Nullable ProducesRequestCondition produces, @Nullable RequestCondition<?> custom) {
77

78
		this.name = (StringUtils.hasText(name) ? name : null);
79 80 81 82 83 84
		this.patternsCondition = (patterns != null ? patterns : new PatternsRequestCondition());
		this.methodsCondition = (methods != null ? methods : new RequestMethodsRequestCondition());
		this.paramsCondition = (params != null ? params : new ParamsRequestCondition());
		this.headersCondition = (headers != null ? headers : new HeadersRequestCondition());
		this.consumesCondition = (consumes != null ? consumes : new ConsumesRequestCondition());
		this.producesCondition = (produces != null ? produces : new ProducesRequestCondition());
85
		this.customConditionHolder = new RequestConditionHolder(custom);
86 87
	}

88 89 90
	/**
	 * Creates a new instance with the given request conditions.
	 */
91 92 93
	public RequestMappingInfo(@Nullable PatternsRequestCondition patterns, @Nullable RequestMethodsRequestCondition methods,
			@Nullable ParamsRequestCondition params, @Nullable HeadersRequestCondition headers, @Nullable ConsumesRequestCondition consumes,
			@Nullable ProducesRequestCondition produces, @Nullable RequestCondition<?> custom) {
94 95 96 97

		this(null, patterns, methods, params, headers, consumes, produces, custom);
	}

98
	/**
99
	 * Re-create a RequestMappingInfo with the given custom request condition.
100
	 */
101
	public RequestMappingInfo(RequestMappingInfo info, @Nullable RequestCondition<?> customRequestCondition) {
102
		this(info.name, info.patternsCondition, info.methodsCondition, info.paramsCondition, info.headersCondition,
103 104 105
				info.consumesCondition, info.producesCondition, customRequestCondition);
	}

106

107 108 109
	/**
	 * Return the name for this mapping, or {@code null}.
	 */
110
	@Nullable
111 112 113 114
	public String getName() {
		return this.name;
	}

115
	/**
J
Juergen Hoeller 已提交
116 117
	 * Return the URL patterns of this {@link RequestMappingInfo};
	 * or instance with 0 patterns (never {@code null}).
118
	 */
119
	public PatternsRequestCondition getPatternsCondition() {
120
		return this.patternsCondition;
121 122 123
	}

	/**
J
Juergen Hoeller 已提交
124 125
	 * Return the HTTP request methods of this {@link RequestMappingInfo};
	 * or instance with 0 request methods (never {@code null}).
126
	 */
127
	public RequestMethodsRequestCondition getMethodsCondition() {
128
		return this.methodsCondition;
129 130 131
	}

	/**
J
Juergen Hoeller 已提交
132 133
	 * Return the "parameters" condition of this {@link RequestMappingInfo};
	 * or instance with 0 parameter expressions (never {@code null}).
134
	 */
135
	public ParamsRequestCondition getParamsCondition() {
136
		return this.paramsCondition;
137 138 139
	}

	/**
J
Juergen Hoeller 已提交
140 141
	 * Return the "headers" condition of this {@link RequestMappingInfo};
	 * or instance with 0 header expressions (never {@code null}).
142
	 */
143
	public HeadersRequestCondition getHeadersCondition() {
144
		return this.headersCondition;
145 146 147
	}

	/**
J
Juergen Hoeller 已提交
148 149
	 * Return the "consumes" condition of this {@link RequestMappingInfo};
	 * or instance with 0 consumes expressions (never {@code null}).
150
	 */
151
	public ConsumesRequestCondition getConsumesCondition() {
152
		return this.consumesCondition;
153 154
	}

155
	/**
J
Juergen Hoeller 已提交
156 157
	 * Return the "produces" condition of this {@link RequestMappingInfo};
	 * or instance with 0 produces expressions (never {@code null}).
158
	 */
159
	public ProducesRequestCondition getProducesCondition() {
160
		return this.producesCondition;
161 162
	}

163
	/**
J
Juergen Hoeller 已提交
164
	 * Return the "custom" condition of this {@link RequestMappingInfo}, or {@code null}.
165
	 */
166
	@Nullable
167
	public RequestCondition<?> getCustomCondition() {
168
		return this.customConditionHolder.getCondition();
169 170
	}

171

172
	/**
J
Juergen Hoeller 已提交
173
	 * Combine "this" request mapping info (i.e. the current instance) with another request mapping info instance.
174 175 176
	 * <p>Example: combine type- and method-level request mappings.
	 * @return a new request mapping info instance; never {@code null}
	 */
177
	@Override
178
	public RequestMappingInfo combine(RequestMappingInfo other) {
179
		String name = combineNames(other);
180 181 182 183 184 185
		PatternsRequestCondition patterns = this.patternsCondition.combine(other.patternsCondition);
		RequestMethodsRequestCondition methods = this.methodsCondition.combine(other.methodsCondition);
		ParamsRequestCondition params = this.paramsCondition.combine(other.paramsCondition);
		HeadersRequestCondition headers = this.headersCondition.combine(other.headersCondition);
		ConsumesRequestCondition consumes = this.consumesCondition.combine(other.consumesCondition);
		ProducesRequestCondition produces = this.producesCondition.combine(other.producesCondition);
186
		RequestConditionHolder custom = this.customConditionHolder.combine(other.customConditionHolder);
187

188 189 190 191
		return new RequestMappingInfo(name, patterns,
				methods, params, headers, consumes, produces, custom.getCondition());
	}

192
	@Nullable
193 194 195 196 197 198 199 200 201
	private String combineNames(RequestMappingInfo other) {
		if (this.name != null && other.name != null) {
			String separator = RequestMappingInfoHandlerMethodMappingNamingStrategy.SEPARATOR;
			return this.name + separator + other.name;
		}
		else if (this.name != null) {
			return this.name;
		}
		else {
J
Juergen Hoeller 已提交
202
			return other.name;
203
		}
204 205 206
	}

	/**
207 208 209
	 * Checks if all conditions in this request mapping info match the provided request and returns
	 * a potentially new request mapping info with conditions tailored to the current request.
	 * <p>For example the returned instance may contain the subset of URL patterns that match to
210 211
	 * the current request, sorted with best matching patterns on top.
	 * @return a new instance in case all conditions match; or {@code null} otherwise
212
	 */
213
	@Override
214
	@Nullable
215
	public RequestMappingInfo getMatchingCondition(HttpServletRequest request) {
216 217 218 219 220
		RequestMethodsRequestCondition methods = this.methodsCondition.getMatchingCondition(request);
		ParamsRequestCondition params = this.paramsCondition.getMatchingCondition(request);
		HeadersRequestCondition headers = this.headersCondition.getMatchingCondition(request);
		ConsumesRequestCondition consumes = this.consumesCondition.getMatchingCondition(request);
		ProducesRequestCondition produces = this.producesCondition.getMatchingCondition(request);
221

222
		if (methods == null || params == null || headers == null || consumes == null || produces == null) {
223
			return null;
224
		}
225

226
		PatternsRequestCondition patterns = this.patternsCondition.getMatchingCondition(request);
227 228
		if (patterns == null) {
			return null;
229
		}
230

231
		RequestConditionHolder custom = this.customConditionHolder.getMatchingCondition(request);
232 233 234
		if (custom == null) {
			return null;
		}
235

236 237
		return new RequestMappingInfo(this.name, patterns,
				methods, params, headers, consumes, produces, custom.getCondition());
238
	}
239

240
	/**
241
	 * Compares "this" info (i.e. the current instance) with another info in the context of a request.
242
	 * <p>Note: It is assumed both instances have been obtained via
243
	 * {@link #getMatchingCondition(HttpServletRequest)} to ensure they have conditions with
244 245
	 * content relevant to current request.
	 */
246
	@Override
247
	public int compareTo(RequestMappingInfo other, HttpServletRequest request) {
248 249 250 251 252 253 254 255 256
		int result;
		// Automatic vs explicit HTTP HEAD mapping
		if (HttpMethod.HEAD.matches(request.getMethod())) {
			result = this.methodsCondition.compareTo(other.getMethodsCondition(), request);
			if (result != 0) {
				return result;
			}
		}
		result = this.patternsCondition.compareTo(other.getPatternsCondition(), request);
257 258 259
		if (result != 0) {
			return result;
		}
260
		result = this.paramsCondition.compareTo(other.getParamsCondition(), request);
261 262 263
		if (result != 0) {
			return result;
		}
264
		result = this.headersCondition.compareTo(other.getHeadersCondition(), request);
265 266 267
		if (result != 0) {
			return result;
		}
268
		result = this.consumesCondition.compareTo(other.getConsumesCondition(), request);
269 270 271
		if (result != 0) {
			return result;
		}
272
		result = this.producesCondition.compareTo(other.getProducesCondition(), request);
273 274 275
		if (result != 0) {
			return result;
		}
276
		// Implicit (no method) vs explicit HTTP method mappings
277
		result = this.methodsCondition.compareTo(other.getMethodsCondition(), request);
278 279 280
		if (result != 0) {
			return result;
		}
281
		result = this.customConditionHolder.compareTo(other.customConditionHolder, request);
282 283 284 285
		if (result != 0) {
			return result;
		}
		return 0;
286 287 288
	}

	@Override
J
Juergen Hoeller 已提交
289 290
	public boolean equals(Object other) {
		if (this == other) {
291 292
			return true;
		}
J
Juergen Hoeller 已提交
293 294 295 296 297 298 299 300 301 302 303
		if (!(other instanceof RequestMappingInfo)) {
			return false;
		}
		RequestMappingInfo otherInfo = (RequestMappingInfo) other;
		return (this.patternsCondition.equals(otherInfo.patternsCondition) &&
				this.methodsCondition.equals(otherInfo.methodsCondition) &&
				this.paramsCondition.equals(otherInfo.paramsCondition) &&
				this.headersCondition.equals(otherInfo.headersCondition) &&
				this.consumesCondition.equals(otherInfo.consumesCondition) &&
				this.producesCondition.equals(otherInfo.producesCondition) &&
				this.customConditionHolder.equals(otherInfo.customConditionHolder));
304 305 306 307
	}

	@Override
	public int hashCode() {
308 309 310 311
		return (this.patternsCondition.hashCode() * 31 +  // primary differentiation
				this.methodsCondition.hashCode() + this.paramsCondition.hashCode() +
				this.headersCondition.hashCode() + this.consumesCondition.hashCode() +
				this.producesCondition.hashCode() + this.customConditionHolder.hashCode());
312 313 314 315 316
	}

	@Override
	public String toString() {
		StringBuilder builder = new StringBuilder("{");
317
		builder.append(this.patternsCondition);
318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335
		if (!this.methodsCondition.isEmpty()) {
			builder.append(",methods=").append(this.methodsCondition);
		}
		if (!this.paramsCondition.isEmpty()) {
			builder.append(",params=").append(this.paramsCondition);
		}
		if (!this.headersCondition.isEmpty()) {
			builder.append(",headers=").append(this.headersCondition);
		}
		if (!this.consumesCondition.isEmpty()) {
			builder.append(",consumes=").append(this.consumesCondition);
		}
		if (!this.producesCondition.isEmpty()) {
			builder.append(",produces=").append(this.producesCondition);
		}
		if (!this.customConditionHolder.isEmpty()) {
			builder.append(",custom=").append(this.customConditionHolder);
		}
336 337 338 339
		builder.append('}');
		return builder.toString();
	}

J
Juergen Hoeller 已提交
340

341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393
	/**
	 * Create a new {@code RequestMappingInfo.Builder} with the given paths.
	 * @param paths the paths to use
	 * @since 4.2
	 */
	public static Builder paths(String... paths) {
		return new DefaultBuilder(paths);
	}


	/**
	 * Defines a builder for creating a RequestMappingInfo.
	 * @since 4.2
	 */
	public interface Builder {

		/**
		 * Set the path patterns.
		 */
		Builder paths(String... paths);

		/**
		 * Set the request method conditions.
		 */
		Builder methods(RequestMethod... methods);

		/**
		 * Set the request param conditions.
		 */
		Builder params(String... params);

		/**
		 * Set the header conditions.
		 * <p>By default this is not set.
		 */
		Builder headers(String... headers);

		/**
		 * Set the consumes conditions.
		 */
		Builder consumes(String... consumes);

		/**
		 * Set the produces conditions.
		 */
		Builder produces(String... produces);

		/**
		 * Set the mapping name.
		 */
		Builder mappingName(String name);

		/**
394
		 * Set a custom condition to use.
395 396 397 398 399 400 401 402 403 404 405 406 407 408
		 */
		Builder customCondition(RequestCondition<?> condition);

		/**
		 * Provide additional configuration needed for request mapping purposes.
		 */
		Builder options(BuilderConfiguration options);

		/**
		 * Build the RequestMappingInfo.
		 */
		RequestMappingInfo build();
	}

J
Juergen Hoeller 已提交
409

410 411
	private static class DefaultBuilder implements Builder {

412
		private String[] paths = new String[0];
413

414
		private RequestMethod[] methods = new RequestMethod[0];
415

416
		private String[] params = new String[0];
417

418
		private String[] headers = new String[0];
419

420
		private String[] consumes = new String[0];
421

422
		private String[] produces = new String[0];
423

424
		@Nullable
425 426
		private String mappingName;

427
		@Nullable
428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499
		private RequestCondition<?> customCondition;

		private BuilderConfiguration options = new BuilderConfiguration();

		public DefaultBuilder(String... paths) {
			this.paths = paths;
		}

		@Override
		public Builder paths(String... paths) {
			this.paths = paths;
			return this;
		}

		@Override
		public DefaultBuilder methods(RequestMethod... methods) {
			this.methods = methods;
			return this;
		}

		@Override
		public DefaultBuilder params(String... params) {
			this.params = params;
			return this;
		}

		@Override
		public DefaultBuilder headers(String... headers) {
			this.headers = headers;
			return this;
		}

		@Override
		public DefaultBuilder consumes(String... consumes) {
			this.consumes = consumes;
			return this;
		}

		@Override
		public DefaultBuilder produces(String... produces) {
			this.produces = produces;
			return this;
		}

		@Override
		public DefaultBuilder mappingName(String name) {
			this.mappingName = name;
			return this;
		}

		@Override
		public DefaultBuilder customCondition(RequestCondition<?> condition) {
			this.customCondition = condition;
			return this;
		}

		@Override
		public Builder options(BuilderConfiguration options) {
			this.options = options;
			return this;
		}

		@Override
		public RequestMappingInfo build() {
			ContentNegotiationManager manager = this.options.getContentNegotiationManager();

			PatternsRequestCondition patternsCondition = new PatternsRequestCondition(
					this.paths, this.options.getUrlPathHelper(), this.options.getPathMatcher(),
					this.options.useSuffixPatternMatch(), this.options.useTrailingSlashMatch(),
					this.options.getFileExtensions());

			return new RequestMappingInfo(this.mappingName, patternsCondition,
500
					new RequestMethodsRequestCondition(this.methods),
501 502 503 504 505 506 507 508
					new ParamsRequestCondition(this.params),
					new HeadersRequestCondition(this.headers),
					new ConsumesRequestCondition(this.consumes, this.headers),
					new ProducesRequestCondition(this.produces, this.headers, manager),
					this.customCondition);
		}
	}

J
Juergen Hoeller 已提交
509

510 511 512 513 514
	/**
	 * Container for configuration options used for request mapping purposes.
	 * Such configuration is required to create RequestMappingInfo instances but
	 * is typically used across all RequestMappingInfo instances.
	 * @since 4.2
J
Juergen Hoeller 已提交
515
	 * @see Builder#options
516 517 518
	 */
	public static class BuilderConfiguration {

519
		@Nullable
520 521
		private UrlPathHelper urlPathHelper;

522
		@Nullable
523 524 525 526 527 528 529 530
		private PathMatcher pathMatcher;

		private boolean trailingSlashMatch = true;

		private boolean suffixPatternMatch = true;

		private boolean registeredSuffixPatternMatch = false;

531
		@Nullable
532 533 534 535 536
		private ContentNegotiationManager contentNegotiationManager;

		/**
		 * Set a custom UrlPathHelper to use for the PatternsRequestCondition.
		 * <p>By default this is not set.
537
		 * @since 4.2.8
538
		 */
539
		public void setUrlPathHelper(@Nullable UrlPathHelper urlPathHelper) {
J
Juergen Hoeller 已提交
540
			this.urlPathHelper = urlPathHelper;
541 542
		}

543 544 545
		/**
		 * Return a custom UrlPathHelper to use for the PatternsRequestCondition, if any.
		 */
546
		@Nullable
547 548 549 550 551 552 553 554
		public UrlPathHelper getUrlPathHelper() {
			return this.urlPathHelper;
		}

		/**
		 * Set a custom PathMatcher to use for the PatternsRequestCondition.
		 * <p>By default this is not set.
		 */
555
		public void setPathMatcher(@Nullable PathMatcher pathMatcher) {
556 557 558
			this.pathMatcher = pathMatcher;
		}

559 560 561
		/**
		 * Return a custom PathMatcher to use for the PatternsRequestCondition, if any.
		 */
562
		@Nullable
563 564 565 566 567
		public PathMatcher getPathMatcher() {
			return this.pathMatcher;
		}

		/**
568
		 * Set whether to apply trailing slash matching in PatternsRequestCondition.
569 570 571 572 573 574
		 * <p>By default this is set to 'true'.
		 */
		public void setTrailingSlashMatch(boolean trailingSlashMatch) {
			this.trailingSlashMatch = trailingSlashMatch;
		}

575 576 577
		/**
		 * Return whether to apply trailing slash matching in PatternsRequestCondition.
		 */
578 579 580 581 582
		public boolean useTrailingSlashMatch() {
			return this.trailingSlashMatch;
		}

		/**
583
		 * Set whether to apply suffix pattern matching in PatternsRequestCondition.
584 585 586 587 588 589 590
		 * <p>By default this is set to 'true'.
		 * @see #setRegisteredSuffixPatternMatch(boolean)
		 */
		public void setSuffixPatternMatch(boolean suffixPatternMatch) {
			this.suffixPatternMatch = suffixPatternMatch;
		}

591 592 593
		/**
		 * Return whether to apply suffix pattern matching in PatternsRequestCondition.
		 */
594 595 596 597 598
		public boolean useSuffixPatternMatch() {
			return this.suffixPatternMatch;
		}

		/**
599
		 * Set whether suffix pattern matching should be restricted to registered
600
		 * file extensions only. Setting this property also sets
601
		 * {@code suffixPatternMatch=true} and requires that a
602 603 604 605 606 607 608 609
		 * {@link #setContentNegotiationManager} is also configured in order to
		 * obtain the registered file extensions.
		 */
		public void setRegisteredSuffixPatternMatch(boolean registeredSuffixPatternMatch) {
			this.registeredSuffixPatternMatch = registeredSuffixPatternMatch;
			this.suffixPatternMatch = (registeredSuffixPatternMatch || this.suffixPatternMatch);
		}

610 611 612 613
		/**
		 * Return whether suffix pattern matching should be restricted to registered
		 * file extensions only.
		 */
614 615 616 617 618 619 620 621 622
		public boolean useRegisteredSuffixPatternMatch() {
			return this.registeredSuffixPatternMatch;
		}

		/**
		 * Return the file extensions to use for suffix pattern matching. If
		 * {@code registeredSuffixPatternMatch=true}, the extensions are obtained
		 * from the configured {@code contentNegotiationManager}.
		 */
623
		@Nullable
624
		public List<String> getFileExtensions() {
625
			if (useRegisteredSuffixPatternMatch() && this.contentNegotiationManager != null) {
626 627 628 629 630 631 632 633 634
				return this.contentNegotiationManager.getAllFileExtensions();
			}
			return null;
		}

		/**
		 * Set the ContentNegotiationManager to use for the ProducesRequestCondition.
		 * <p>By default this is not set.
		 */
635 636
		public void setContentNegotiationManager(ContentNegotiationManager contentNegotiationManager) {
			this.contentNegotiationManager = contentNegotiationManager;
637 638
		}

639 640 641 642
		/**
		 * Return the ContentNegotiationManager to use for the ProducesRequestCondition,
		 * if any.
		 */
643
		@Nullable
644 645 646 647 648
		public ContentNegotiationManager getContentNegotiationManager() {
			return this.contentNegotiationManager;
		}
	}

649
}