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}