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}