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.mock.web; 018 019import java.io.BufferedReader; 020import java.io.ByteArrayInputStream; 021import java.io.IOException; 022import java.io.InputStream; 023import java.io.InputStreamReader; 024import java.io.Reader; 025import java.io.StringReader; 026import java.io.UnsupportedEncodingException; 027import java.security.Principal; 028import java.text.ParseException; 029import java.text.SimpleDateFormat; 030import java.util.Arrays; 031import java.util.Collection; 032import java.util.Collections; 033import java.util.Date; 034import java.util.Enumeration; 035import java.util.HashSet; 036import java.util.LinkedHashMap; 037import java.util.LinkedHashSet; 038import java.util.LinkedList; 039import java.util.List; 040import java.util.Locale; 041import java.util.Map; 042import java.util.Set; 043import java.util.TimeZone; 044import java.util.stream.Collectors; 045 046import javax.servlet.AsyncContext; 047import javax.servlet.DispatcherType; 048import javax.servlet.RequestDispatcher; 049import javax.servlet.ServletContext; 050import javax.servlet.ServletException; 051import javax.servlet.ServletInputStream; 052import javax.servlet.ServletRequest; 053import javax.servlet.ServletResponse; 054import javax.servlet.http.Cookie; 055import javax.servlet.http.HttpServletRequest; 056import javax.servlet.http.HttpServletResponse; 057import javax.servlet.http.HttpSession; 058import javax.servlet.http.HttpUpgradeHandler; 059import javax.servlet.http.Part; 060 061import org.springframework.http.HttpHeaders; 062import org.springframework.http.MediaType; 063import org.springframework.lang.NonNull; 064import org.springframework.lang.Nullable; 065import org.springframework.util.Assert; 066import org.springframework.util.LinkedCaseInsensitiveMap; 067import org.springframework.util.LinkedMultiValueMap; 068import org.springframework.util.MultiValueMap; 069import org.springframework.util.ObjectUtils; 070import org.springframework.util.StreamUtils; 071import org.springframework.util.StringUtils; 072 073/** 074 * Mock implementation of the {@link javax.servlet.http.HttpServletRequest} interface. 075 * 076 * <p>The default, preferred {@link Locale} for the <em>server</em> mocked by this request 077 * is {@link Locale#ENGLISH}. This value can be changed via {@link #addPreferredLocale} 078 * or {@link #setPreferredLocales}. 079 * 080 * <p>As of Spring Framework 5.0, this set of mocks is designed on a Servlet 4.0 baseline. 081 * 082 * @author Juergen Hoeller 083 * @author Rod Johnson 084 * @author Rick Evans 085 * @author Mark Fisher 086 * @author Chris Beams 087 * @author Sam Brannen 088 * @author Brian Clozel 089 * @since 1.0.2 090 */ 091public class MockHttpServletRequest implements HttpServletRequest { 092 093 private static final String HTTP = "http"; 094 095 private static final String HTTPS = "https"; 096 097 private static final String CHARSET_PREFIX = "charset="; 098 099 private static final TimeZone GMT = TimeZone.getTimeZone("GMT"); 100 101 private static final ServletInputStream EMPTY_SERVLET_INPUT_STREAM = 102 new DelegatingServletInputStream(StreamUtils.emptyInput()); 103 104 private static final BufferedReader EMPTY_BUFFERED_READER = 105 new BufferedReader(new StringReader("")); 106 107 /** 108 * Date formats as specified in the HTTP RFC. 109 * @see <a href="https://tools.ietf.org/html/rfc7231#section-7.1.1.1">Section 7.1.1.1 of RFC 7231</a> 110 */ 111 private static final String[] DATE_FORMATS = new String[] { 112 "EEE, dd MMM yyyy HH:mm:ss zzz", 113 "EEE, dd-MMM-yy HH:mm:ss zzz", 114 "EEE MMM dd HH:mm:ss yyyy" 115 }; 116 117 118 // --------------------------------------------------------------------- 119 // Public constants 120 // --------------------------------------------------------------------- 121 122 /** 123 * The default protocol: 'HTTP/1.1'. 124 * @since 4.3.7 125 */ 126 public static final String DEFAULT_PROTOCOL = "HTTP/1.1"; 127 128 /** 129 * The default scheme: 'http'. 130 * @since 4.3.7 131 */ 132 public static final String DEFAULT_SCHEME = HTTP; 133 134 /** 135 * The default server address: '127.0.0.1'. 136 */ 137 public static final String DEFAULT_SERVER_ADDR = "127.0.0.1"; 138 139 /** 140 * The default server name: 'localhost'. 141 */ 142 public static final String DEFAULT_SERVER_NAME = "localhost"; 143 144 /** 145 * The default server port: '80'. 146 */ 147 public static final int DEFAULT_SERVER_PORT = 80; 148 149 /** 150 * The default remote address: '127.0.0.1'. 151 */ 152 public static final String DEFAULT_REMOTE_ADDR = "127.0.0.1"; 153 154 /** 155 * The default remote host: 'localhost'. 156 */ 157 public static final String DEFAULT_REMOTE_HOST = "localhost"; 158 159 160 // --------------------------------------------------------------------- 161 // Lifecycle properties 162 // --------------------------------------------------------------------- 163 164 private final ServletContext servletContext; 165 166 private boolean active = true; 167 168 169 // --------------------------------------------------------------------- 170 // ServletRequest properties 171 // --------------------------------------------------------------------- 172 173 private final Map<String, Object> attributes = new LinkedHashMap<>(); 174 175 @Nullable 176 private String characterEncoding; 177 178 @Nullable 179 private byte[] content; 180 181 @Nullable 182 private String contentType; 183 184 @Nullable 185 private ServletInputStream inputStream; 186 187 @Nullable 188 private BufferedReader reader; 189 190 private final Map<String, String[]> parameters = new LinkedHashMap<>(16); 191 192 private String protocol = DEFAULT_PROTOCOL; 193 194 private String scheme = DEFAULT_SCHEME; 195 196 private String serverName = DEFAULT_SERVER_NAME; 197 198 private int serverPort = DEFAULT_SERVER_PORT; 199 200 private String remoteAddr = DEFAULT_REMOTE_ADDR; 201 202 private String remoteHost = DEFAULT_REMOTE_HOST; 203 204 /** List of locales in descending order. */ 205 private final LinkedList<Locale> locales = new LinkedList<>(); 206 207 private boolean secure = false; 208 209 private int remotePort = DEFAULT_SERVER_PORT; 210 211 private String localName = DEFAULT_SERVER_NAME; 212 213 private String localAddr = DEFAULT_SERVER_ADDR; 214 215 private int localPort = DEFAULT_SERVER_PORT; 216 217 private boolean asyncStarted = false; 218 219 private boolean asyncSupported = false; 220 221 @Nullable 222 private MockAsyncContext asyncContext; 223 224 private DispatcherType dispatcherType = DispatcherType.REQUEST; 225 226 227 // --------------------------------------------------------------------- 228 // HttpServletRequest properties 229 // --------------------------------------------------------------------- 230 231 @Nullable 232 private String authType; 233 234 @Nullable 235 private Cookie[] cookies; 236 237 private final Map<String, HeaderValueHolder> headers = new LinkedCaseInsensitiveMap<>(); 238 239 @Nullable 240 private String method; 241 242 @Nullable 243 private String pathInfo; 244 245 private String contextPath = ""; 246 247 @Nullable 248 private String queryString; 249 250 @Nullable 251 private String remoteUser; 252 253 private final Set<String> userRoles = new HashSet<>(); 254 255 @Nullable 256 private Principal userPrincipal; 257 258 @Nullable 259 private String requestedSessionId; 260 261 @Nullable 262 private String requestURI; 263 264 private String servletPath = ""; 265 266 @Nullable 267 private HttpSession session; 268 269 private boolean requestedSessionIdValid = true; 270 271 private boolean requestedSessionIdFromCookie = true; 272 273 private boolean requestedSessionIdFromURL = false; 274 275 private final MultiValueMap<String, Part> parts = new LinkedMultiValueMap<>(); 276 277 278 // --------------------------------------------------------------------- 279 // Constructors 280 // --------------------------------------------------------------------- 281 282 /** 283 * Create a new {@code MockHttpServletRequest} with a default 284 * {@link MockServletContext}. 285 * @see #MockHttpServletRequest(ServletContext, String, String) 286 */ 287 public MockHttpServletRequest() { 288 this(null, "", ""); 289 } 290 291 /** 292 * Create a new {@code MockHttpServletRequest} with a default 293 * {@link MockServletContext}. 294 * @param method the request method (may be {@code null}) 295 * @param requestURI the request URI (may be {@code null}) 296 * @see #setMethod 297 * @see #setRequestURI 298 * @see #MockHttpServletRequest(ServletContext, String, String) 299 */ 300 public MockHttpServletRequest(@Nullable String method, @Nullable String requestURI) { 301 this(null, method, requestURI); 302 } 303 304 /** 305 * Create a new {@code MockHttpServletRequest} with the supplied {@link ServletContext}. 306 * @param servletContext the ServletContext that the request runs in 307 * (may be {@code null} to use a default {@link MockServletContext}) 308 * @see #MockHttpServletRequest(ServletContext, String, String) 309 */ 310 public MockHttpServletRequest(@Nullable ServletContext servletContext) { 311 this(servletContext, "", ""); 312 } 313 314 /** 315 * Create a new {@code MockHttpServletRequest} with the supplied {@link ServletContext}, 316 * {@code method}, and {@code requestURI}. 317 * <p>The preferred locale will be set to {@link Locale#ENGLISH}. 318 * @param servletContext the ServletContext that the request runs in (may be 319 * {@code null} to use a default {@link MockServletContext}) 320 * @param method the request method (may be {@code null}) 321 * @param requestURI the request URI (may be {@code null}) 322 * @see #setMethod 323 * @see #setRequestURI 324 * @see #setPreferredLocales 325 * @see MockServletContext 326 */ 327 public MockHttpServletRequest(@Nullable ServletContext servletContext, @Nullable String method, @Nullable String requestURI) { 328 this.servletContext = (servletContext != null ? servletContext : new MockServletContext()); 329 this.method = method; 330 this.requestURI = requestURI; 331 this.locales.add(Locale.ENGLISH); 332 } 333 334 335 // --------------------------------------------------------------------- 336 // Lifecycle methods 337 // --------------------------------------------------------------------- 338 339 /** 340 * Return the ServletContext that this request is associated with. (Not 341 * available in the standard HttpServletRequest interface for some reason.) 342 */ 343 @Override 344 public ServletContext getServletContext() { 345 return this.servletContext; 346 } 347 348 /** 349 * Return whether this request is still active (that is, not completed yet). 350 */ 351 public boolean isActive() { 352 return this.active; 353 } 354 355 /** 356 * Mark this request as completed, keeping its state. 357 */ 358 public void close() { 359 this.active = false; 360 } 361 362 /** 363 * Invalidate this request, clearing its state. 364 */ 365 public void invalidate() { 366 close(); 367 clearAttributes(); 368 } 369 370 /** 371 * Check whether this request is still active (that is, not completed yet), 372 * throwing an IllegalStateException if not active anymore. 373 */ 374 protected void checkActive() throws IllegalStateException { 375 Assert.state(this.active, "Request is not active anymore"); 376 } 377 378 379 // --------------------------------------------------------------------- 380 // ServletRequest interface 381 // --------------------------------------------------------------------- 382 383 @Override 384 public Object getAttribute(String name) { 385 checkActive(); 386 return this.attributes.get(name); 387 } 388 389 @Override 390 public Enumeration<String> getAttributeNames() { 391 checkActive(); 392 return Collections.enumeration(new LinkedHashSet<>(this.attributes.keySet())); 393 } 394 395 @Override 396 @Nullable 397 public String getCharacterEncoding() { 398 return this.characterEncoding; 399 } 400 401 @Override 402 public void setCharacterEncoding(@Nullable String characterEncoding) { 403 this.characterEncoding = characterEncoding; 404 updateContentTypeHeader(); 405 } 406 407 private void updateContentTypeHeader() { 408 if (StringUtils.hasLength(this.contentType)) { 409 String value = this.contentType; 410 if (StringUtils.hasLength(this.characterEncoding) && !this.contentType.toLowerCase().contains(CHARSET_PREFIX)) { 411 value += ';' + CHARSET_PREFIX + this.characterEncoding; 412 } 413 doAddHeaderValue(HttpHeaders.CONTENT_TYPE, value, true); 414 } 415 } 416 417 /** 418 * Set the content of the request body as a byte array. 419 * <p>If the supplied byte array represents text such as XML or JSON, the 420 * {@link #setCharacterEncoding character encoding} should typically be 421 * set as well. 422 * @see #setCharacterEncoding(String) 423 * @see #getContentAsByteArray() 424 * @see #getContentAsString() 425 */ 426 public void setContent(@Nullable byte[] content) { 427 this.content = content; 428 this.inputStream = null; 429 this.reader = null; 430 } 431 432 /** 433 * Get the content of the request body as a byte array. 434 * @return the content as a byte array (potentially {@code null}) 435 * @since 5.0 436 * @see #setContent(byte[]) 437 * @see #getContentAsString() 438 */ 439 @Nullable 440 public byte[] getContentAsByteArray() { 441 return this.content; 442 } 443 444 /** 445 * Get the content of the request body as a {@code String}, using the configured 446 * {@linkplain #getCharacterEncoding character encoding}. 447 * @return the content as a {@code String}, potentially {@code null} 448 * @throws IllegalStateException if the character encoding has not been set 449 * @throws UnsupportedEncodingException if the character encoding is not supported 450 * @since 5.0 451 * @see #setContent(byte[]) 452 * @see #setCharacterEncoding(String) 453 * @see #getContentAsByteArray() 454 */ 455 @Nullable 456 public String getContentAsString() throws IllegalStateException, UnsupportedEncodingException { 457 Assert.state(this.characterEncoding != null, 458 "Cannot get content as a String for a null character encoding. " + 459 "Consider setting the characterEncoding in the request."); 460 461 if (this.content == null) { 462 return null; 463 } 464 return new String(this.content, this.characterEncoding); 465 } 466 467 @Override 468 public int getContentLength() { 469 return (this.content != null ? this.content.length : -1); 470 } 471 472 @Override 473 public long getContentLengthLong() { 474 return getContentLength(); 475 } 476 477 public void setContentType(@Nullable String contentType) { 478 this.contentType = contentType; 479 if (contentType != null) { 480 try { 481 MediaType mediaType = MediaType.parseMediaType(contentType); 482 if (mediaType.getCharset() != null) { 483 this.characterEncoding = mediaType.getCharset().name(); 484 } 485 } 486 catch (IllegalArgumentException ex) { 487 // Try to get charset value anyway 488 int charsetIndex = contentType.toLowerCase().indexOf(CHARSET_PREFIX); 489 if (charsetIndex != -1) { 490 this.characterEncoding = contentType.substring(charsetIndex + CHARSET_PREFIX.length()); 491 } 492 } 493 updateContentTypeHeader(); 494 } 495 } 496 497 @Override 498 @Nullable 499 public String getContentType() { 500 return this.contentType; 501 } 502 503 @Override 504 public ServletInputStream getInputStream() { 505 if (this.inputStream != null) { 506 return this.inputStream; 507 } 508 else if (this.reader != null) { 509 throw new IllegalStateException( 510 "Cannot call getInputStream() after getReader() has already been called for the current request") ; 511 } 512 513 this.inputStream = (this.content != null ? 514 new DelegatingServletInputStream(new ByteArrayInputStream(this.content)) : 515 EMPTY_SERVLET_INPUT_STREAM); 516 return this.inputStream; 517 } 518 519 /** 520 * Set a single value for the specified HTTP parameter. 521 * <p>If there are already one or more values registered for the given 522 * parameter name, they will be replaced. 523 */ 524 public void setParameter(String name, String value) { 525 setParameter(name, new String[] {value}); 526 } 527 528 /** 529 * Set an array of values for the specified HTTP parameter. 530 * <p>If there are already one or more values registered for the given 531 * parameter name, they will be replaced. 532 */ 533 public void setParameter(String name, String... values) { 534 Assert.notNull(name, "Parameter name must not be null"); 535 this.parameters.put(name, values); 536 } 537 538 /** 539 * Set all provided parameters <strong>replacing</strong> any existing 540 * values for the provided parameter names. To add without replacing 541 * existing values, use {@link #addParameters(java.util.Map)}. 542 */ 543 public void setParameters(Map<String, ?> params) { 544 Assert.notNull(params, "Parameter map must not be null"); 545 params.forEach((key, value) -> { 546 if (value instanceof String) { 547 setParameter(key, (String) value); 548 } 549 else if (value instanceof String[]) { 550 setParameter(key, (String[]) value); 551 } 552 else { 553 throw new IllegalArgumentException( 554 "Parameter map value must be single value " + " or array of type [" + String.class.getName() + "]"); 555 } 556 }); 557 } 558 559 /** 560 * Add a single value for the specified HTTP parameter. 561 * <p>If there are already one or more values registered for the given 562 * parameter name, the given value will be added to the end of the list. 563 */ 564 public void addParameter(String name, @Nullable String value) { 565 addParameter(name, new String[] {value}); 566 } 567 568 /** 569 * Add an array of values for the specified HTTP parameter. 570 * <p>If there are already one or more values registered for the given 571 * parameter name, the given values will be added to the end of the list. 572 */ 573 public void addParameter(String name, String... values) { 574 Assert.notNull(name, "Parameter name must not be null"); 575 String[] oldArr = this.parameters.get(name); 576 if (oldArr != null) { 577 String[] newArr = new String[oldArr.length + values.length]; 578 System.arraycopy(oldArr, 0, newArr, 0, oldArr.length); 579 System.arraycopy(values, 0, newArr, oldArr.length, values.length); 580 this.parameters.put(name, newArr); 581 } 582 else { 583 this.parameters.put(name, values); 584 } 585 } 586 587 /** 588 * Add all provided parameters <strong>without</strong> replacing any 589 * existing values. To replace existing values, use 590 * {@link #setParameters(java.util.Map)}. 591 */ 592 public void addParameters(Map<String, ?> params) { 593 Assert.notNull(params, "Parameter map must not be null"); 594 params.forEach((key, value) -> { 595 if (value instanceof String) { 596 addParameter(key, (String) value); 597 } 598 else if (value instanceof String[]) { 599 addParameter(key, (String[]) value); 600 } 601 else { 602 throw new IllegalArgumentException("Parameter map value must be single value " + 603 " or array of type [" + String.class.getName() + "]"); 604 } 605 }); 606 } 607 608 /** 609 * Remove already registered values for the specified HTTP parameter, if any. 610 */ 611 public void removeParameter(String name) { 612 Assert.notNull(name, "Parameter name must not be null"); 613 this.parameters.remove(name); 614 } 615 616 /** 617 * Remove all existing parameters. 618 */ 619 public void removeAllParameters() { 620 this.parameters.clear(); 621 } 622 623 @Override 624 @Nullable 625 public String getParameter(String name) { 626 Assert.notNull(name, "Parameter name must not be null"); 627 String[] arr = this.parameters.get(name); 628 return (arr != null && arr.length > 0 ? arr[0] : null); 629 } 630 631 @Override 632 public Enumeration<String> getParameterNames() { 633 return Collections.enumeration(this.parameters.keySet()); 634 } 635 636 @Override 637 public String[] getParameterValues(String name) { 638 Assert.notNull(name, "Parameter name must not be null"); 639 return this.parameters.get(name); 640 } 641 642 @Override 643 public Map<String, String[]> getParameterMap() { 644 return Collections.unmodifiableMap(this.parameters); 645 } 646 647 public void setProtocol(String protocol) { 648 this.protocol = protocol; 649 } 650 651 @Override 652 public String getProtocol() { 653 return this.protocol; 654 } 655 656 public void setScheme(String scheme) { 657 this.scheme = scheme; 658 } 659 660 @Override 661 public String getScheme() { 662 return this.scheme; 663 } 664 665 public void setServerName(String serverName) { 666 this.serverName = serverName; 667 } 668 669 @Override 670 public String getServerName() { 671 String rawHostHeader = getHeader(HttpHeaders.HOST); 672 String host = rawHostHeader; 673 if (host != null) { 674 host = host.trim(); 675 if (host.startsWith("[")) { 676 int indexOfClosingBracket = host.indexOf(']'); 677 Assert.state(indexOfClosingBracket > -1, () -> "Invalid Host header: " + rawHostHeader); 678 host = host.substring(0, indexOfClosingBracket + 1); 679 } 680 else if (host.contains(":")) { 681 host = host.substring(0, host.indexOf(':')); 682 } 683 return host; 684 } 685 686 // else 687 return this.serverName; 688 } 689 690 public void setServerPort(int serverPort) { 691 this.serverPort = serverPort; 692 } 693 694 @Override 695 public int getServerPort() { 696 String rawHostHeader = getHeader(HttpHeaders.HOST); 697 String host = rawHostHeader; 698 if (host != null) { 699 host = host.trim(); 700 int idx; 701 if (host.startsWith("[")) { 702 int indexOfClosingBracket = host.indexOf(']'); 703 Assert.state(indexOfClosingBracket > -1, () -> "Invalid Host header: " + rawHostHeader); 704 idx = host.indexOf(':', indexOfClosingBracket); 705 } 706 else { 707 idx = host.indexOf(':'); 708 } 709 if (idx != -1) { 710 return Integer.parseInt(host.substring(idx + 1)); 711 } 712 } 713 714 // else 715 return this.serverPort; 716 } 717 718 @Override 719 public BufferedReader getReader() throws UnsupportedEncodingException { 720 if (this.reader != null) { 721 return this.reader; 722 } 723 else if (this.inputStream != null) { 724 throw new IllegalStateException( 725 "Cannot call getReader() after getInputStream() has already been called for the current request") ; 726 } 727 728 if (this.content != null) { 729 InputStream sourceStream = new ByteArrayInputStream(this.content); 730 Reader sourceReader = (this.characterEncoding != null) ? 731 new InputStreamReader(sourceStream, this.characterEncoding) : 732 new InputStreamReader(sourceStream); 733 this.reader = new BufferedReader(sourceReader); 734 } 735 else { 736 this.reader = EMPTY_BUFFERED_READER; 737 } 738 return this.reader; 739 } 740 741 public void setRemoteAddr(String remoteAddr) { 742 this.remoteAddr = remoteAddr; 743 } 744 745 @Override 746 public String getRemoteAddr() { 747 return this.remoteAddr; 748 } 749 750 public void setRemoteHost(String remoteHost) { 751 this.remoteHost = remoteHost; 752 } 753 754 @Override 755 public String getRemoteHost() { 756 return this.remoteHost; 757 } 758 759 @Override 760 public void setAttribute(String name, @Nullable Object value) { 761 checkActive(); 762 Assert.notNull(name, "Attribute name must not be null"); 763 if (value != null) { 764 this.attributes.put(name, value); 765 } 766 else { 767 this.attributes.remove(name); 768 } 769 } 770 771 @Override 772 public void removeAttribute(String name) { 773 checkActive(); 774 Assert.notNull(name, "Attribute name must not be null"); 775 this.attributes.remove(name); 776 } 777 778 /** 779 * Clear all of this request's attributes. 780 */ 781 public void clearAttributes() { 782 this.attributes.clear(); 783 } 784 785 /** 786 * Add a new preferred locale, before any existing locales. 787 * @see #setPreferredLocales 788 */ 789 public void addPreferredLocale(Locale locale) { 790 Assert.notNull(locale, "Locale must not be null"); 791 this.locales.addFirst(locale); 792 updateAcceptLanguageHeader(); 793 } 794 795 /** 796 * Set the list of preferred locales, in descending order, effectively replacing 797 * any existing locales. 798 * @since 3.2 799 * @see #addPreferredLocale 800 */ 801 public void setPreferredLocales(List<Locale> locales) { 802 Assert.notEmpty(locales, "Locale list must not be empty"); 803 this.locales.clear(); 804 this.locales.addAll(locales); 805 updateAcceptLanguageHeader(); 806 } 807 808 private void updateAcceptLanguageHeader() { 809 HttpHeaders headers = new HttpHeaders(); 810 headers.setAcceptLanguageAsLocales(this.locales); 811 doAddHeaderValue(HttpHeaders.ACCEPT_LANGUAGE, headers.getFirst(HttpHeaders.ACCEPT_LANGUAGE), true); 812 } 813 814 /** 815 * Return the first preferred {@linkplain Locale locale} configured 816 * in this mock request. 817 * <p>If no locales have been explicitly configured, the default, 818 * preferred {@link Locale} for the <em>server</em> mocked by this 819 * request is {@link Locale#ENGLISH}. 820 * <p>In contrast to the Servlet specification, this mock implementation 821 * does <strong>not</strong> take into consideration any locales 822 * specified via the {@code Accept-Language} header. 823 * @see javax.servlet.ServletRequest#getLocale() 824 * @see #addPreferredLocale(Locale) 825 * @see #setPreferredLocales(List) 826 */ 827 @Override 828 public Locale getLocale() { 829 return this.locales.getFirst(); 830 } 831 832 /** 833 * Return an {@linkplain Enumeration enumeration} of the preferred 834 * {@linkplain Locale locales} configured in this mock request. 835 * <p>If no locales have been explicitly configured, the default, 836 * preferred {@link Locale} for the <em>server</em> mocked by this 837 * request is {@link Locale#ENGLISH}. 838 * <p>In contrast to the Servlet specification, this mock implementation 839 * does <strong>not</strong> take into consideration any locales 840 * specified via the {@code Accept-Language} header. 841 * @see javax.servlet.ServletRequest#getLocales() 842 * @see #addPreferredLocale(Locale) 843 * @see #setPreferredLocales(List) 844 */ 845 @Override 846 public Enumeration<Locale> getLocales() { 847 return Collections.enumeration(this.locales); 848 } 849 850 /** 851 * Set the boolean {@code secure} flag indicating whether the mock request 852 * was made using a secure channel, such as HTTPS. 853 * @see #isSecure() 854 * @see #getScheme() 855 * @see #setScheme(String) 856 */ 857 public void setSecure(boolean secure) { 858 this.secure = secure; 859 } 860 861 /** 862 * Return {@code true} if the {@link #setSecure secure} flag has been set 863 * to {@code true} or if the {@link #getScheme scheme} is {@code https}. 864 * @see javax.servlet.ServletRequest#isSecure() 865 */ 866 @Override 867 public boolean isSecure() { 868 return (this.secure || HTTPS.equalsIgnoreCase(this.scheme)); 869 } 870 871 @Override 872 public RequestDispatcher getRequestDispatcher(String path) { 873 return new MockRequestDispatcher(path); 874 } 875 876 @Override 877 @Deprecated 878 public String getRealPath(String path) { 879 return this.servletContext.getRealPath(path); 880 } 881 882 public void setRemotePort(int remotePort) { 883 this.remotePort = remotePort; 884 } 885 886 @Override 887 public int getRemotePort() { 888 return this.remotePort; 889 } 890 891 public void setLocalName(String localName) { 892 this.localName = localName; 893 } 894 895 @Override 896 public String getLocalName() { 897 return this.localName; 898 } 899 900 public void setLocalAddr(String localAddr) { 901 this.localAddr = localAddr; 902 } 903 904 @Override 905 public String getLocalAddr() { 906 return this.localAddr; 907 } 908 909 public void setLocalPort(int localPort) { 910 this.localPort = localPort; 911 } 912 913 @Override 914 public int getLocalPort() { 915 return this.localPort; 916 } 917 918 @Override 919 public AsyncContext startAsync() { 920 return startAsync(this, null); 921 } 922 923 @Override 924 public AsyncContext startAsync(ServletRequest request, @Nullable ServletResponse response) { 925 Assert.state(this.asyncSupported, "Async not supported"); 926 this.asyncStarted = true; 927 this.asyncContext = new MockAsyncContext(request, response); 928 return this.asyncContext; 929 } 930 931 public void setAsyncStarted(boolean asyncStarted) { 932 this.asyncStarted = asyncStarted; 933 } 934 935 @Override 936 public boolean isAsyncStarted() { 937 return this.asyncStarted; 938 } 939 940 public void setAsyncSupported(boolean asyncSupported) { 941 this.asyncSupported = asyncSupported; 942 } 943 944 @Override 945 public boolean isAsyncSupported() { 946 return this.asyncSupported; 947 } 948 949 public void setAsyncContext(@Nullable MockAsyncContext asyncContext) { 950 this.asyncContext = asyncContext; 951 } 952 953 @Override 954 @Nullable 955 public AsyncContext getAsyncContext() { 956 return this.asyncContext; 957 } 958 959 public void setDispatcherType(DispatcherType dispatcherType) { 960 this.dispatcherType = dispatcherType; 961 } 962 963 @Override 964 public DispatcherType getDispatcherType() { 965 return this.dispatcherType; 966 } 967 968 969 // --------------------------------------------------------------------- 970 // HttpServletRequest interface 971 // --------------------------------------------------------------------- 972 973 public void setAuthType(@Nullable String authType) { 974 this.authType = authType; 975 } 976 977 @Override 978 @Nullable 979 public String getAuthType() { 980 return this.authType; 981 } 982 983 public void setCookies(@Nullable Cookie... cookies) { 984 this.cookies = (ObjectUtils.isEmpty(cookies) ? null : cookies); 985 if (this.cookies == null) { 986 removeHeader(HttpHeaders.COOKIE); 987 } 988 else { 989 doAddHeaderValue(HttpHeaders.COOKIE, encodeCookies(this.cookies), true); 990 } 991 } 992 993 private static String encodeCookies(@NonNull Cookie... cookies) { 994 return Arrays.stream(cookies) 995 .map(c -> c.getName() + '=' + (c.getValue() == null ? "" : c.getValue())) 996 .collect(Collectors.joining("; ")); 997 } 998 999 @Override 1000 @Nullable 1001 public Cookie[] getCookies() { 1002 return this.cookies; 1003 } 1004 1005 /** 1006 * Add an HTTP header entry for the given name. 1007 * <p>While this method can take any {@code Object} as a parameter, 1008 * it is recommended to use the following types: 1009 * <ul> 1010 * <li>String or any Object to be converted using {@code toString()}; see {@link #getHeader}.</li> 1011 * <li>String, Number, or Date for date headers; see {@link #getDateHeader}.</li> 1012 * <li>String or Number for integer headers; see {@link #getIntHeader}.</li> 1013 * <li>{@code String[]} or {@code Collection<String>} for multiple values; see {@link #getHeaders}.</li> 1014 * </ul> 1015 * @see #getHeaderNames 1016 * @see #getHeaders 1017 * @see #getHeader 1018 * @see #getDateHeader 1019 */ 1020 public void addHeader(String name, Object value) { 1021 if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name) && 1022 !this.headers.containsKey(HttpHeaders.CONTENT_TYPE)) { 1023 setContentType(value.toString()); 1024 } 1025 else if (HttpHeaders.ACCEPT_LANGUAGE.equalsIgnoreCase(name) && 1026 !this.headers.containsKey(HttpHeaders.ACCEPT_LANGUAGE)) { 1027 try { 1028 HttpHeaders headers = new HttpHeaders(); 1029 headers.add(HttpHeaders.ACCEPT_LANGUAGE, value.toString()); 1030 List<Locale> locales = headers.getAcceptLanguageAsLocales(); 1031 this.locales.clear(); 1032 this.locales.addAll(locales); 1033 if (this.locales.isEmpty()) { 1034 this.locales.add(Locale.ENGLISH); 1035 } 1036 } 1037 catch (IllegalArgumentException ex) { 1038 // Invalid Accept-Language format -> just store plain header 1039 } 1040 doAddHeaderValue(name, value, true); 1041 } 1042 else { 1043 doAddHeaderValue(name, value, false); 1044 } 1045 } 1046 1047 private void doAddHeaderValue(String name, @Nullable Object value, boolean replace) { 1048 HeaderValueHolder header = this.headers.get(name); 1049 Assert.notNull(value, "Header value must not be null"); 1050 if (header == null || replace) { 1051 header = new HeaderValueHolder(); 1052 this.headers.put(name, header); 1053 } 1054 if (value instanceof Collection) { 1055 header.addValues((Collection<?>) value); 1056 } 1057 else if (value.getClass().isArray()) { 1058 header.addValueArray(value); 1059 } 1060 else { 1061 header.addValue(value); 1062 } 1063 } 1064 1065 /** 1066 * Remove already registered entries for the specified HTTP header, if any. 1067 * @since 4.3.20 1068 */ 1069 public void removeHeader(String name) { 1070 Assert.notNull(name, "Header name must not be null"); 1071 this.headers.remove(name); 1072 } 1073 1074 /** 1075 * Return the long timestamp for the date header with the given {@code name}. 1076 * <p>If the internal value representation is a String, this method will try 1077 * to parse it as a date using the supported date formats: 1078 * <ul> 1079 * <li>"EEE, dd MMM yyyy HH:mm:ss zzz"</li> 1080 * <li>"EEE, dd-MMM-yy HH:mm:ss zzz"</li> 1081 * <li>"EEE MMM dd HH:mm:ss yyyy"</li> 1082 * </ul> 1083 * @param name the header name 1084 * @see <a href="https://tools.ietf.org/html/rfc7231#section-7.1.1.1">Section 7.1.1.1 of RFC 7231</a> 1085 */ 1086 @Override 1087 public long getDateHeader(String name) { 1088 HeaderValueHolder header = this.headers.get(name); 1089 Object value = (header != null ? header.getValue() : null); 1090 if (value instanceof Date) { 1091 return ((Date) value).getTime(); 1092 } 1093 else if (value instanceof Number) { 1094 return ((Number) value).longValue(); 1095 } 1096 else if (value instanceof String) { 1097 return parseDateHeader(name, (String) value); 1098 } 1099 else if (value != null) { 1100 throw new IllegalArgumentException( 1101 "Value for header '" + name + "' is not a Date, Number, or String: " + value); 1102 } 1103 else { 1104 return -1L; 1105 } 1106 } 1107 1108 private long parseDateHeader(String name, String value) { 1109 for (String dateFormat : DATE_FORMATS) { 1110 SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat, Locale.US); 1111 simpleDateFormat.setTimeZone(GMT); 1112 try { 1113 return simpleDateFormat.parse(value).getTime(); 1114 } 1115 catch (ParseException ex) { 1116 // ignore 1117 } 1118 } 1119 throw new IllegalArgumentException("Cannot parse date value '" + value + "' for '" + name + "' header"); 1120 } 1121 1122 @Override 1123 @Nullable 1124 public String getHeader(String name) { 1125 HeaderValueHolder header = this.headers.get(name); 1126 return (header != null ? header.getStringValue() : null); 1127 } 1128 1129 @Override 1130 public Enumeration<String> getHeaders(String name) { 1131 HeaderValueHolder header = this.headers.get(name); 1132 return Collections.enumeration(header != null ? header.getStringValues() : new LinkedList<>()); 1133 } 1134 1135 @Override 1136 public Enumeration<String> getHeaderNames() { 1137 return Collections.enumeration(this.headers.keySet()); 1138 } 1139 1140 @Override 1141 public int getIntHeader(String name) { 1142 HeaderValueHolder header = this.headers.get(name); 1143 Object value = (header != null ? header.getValue() : null); 1144 if (value instanceof Number) { 1145 return ((Number) value).intValue(); 1146 } 1147 else if (value instanceof String) { 1148 return Integer.parseInt((String) value); 1149 } 1150 else if (value != null) { 1151 throw new NumberFormatException("Value for header '" + name + "' is not a Number: " + value); 1152 } 1153 else { 1154 return -1; 1155 } 1156 } 1157 1158 public void setMethod(@Nullable String method) { 1159 this.method = method; 1160 } 1161 1162 @Override 1163 @Nullable 1164 public String getMethod() { 1165 return this.method; 1166 } 1167 1168 public void setPathInfo(@Nullable String pathInfo) { 1169 this.pathInfo = pathInfo; 1170 } 1171 1172 @Override 1173 @Nullable 1174 public String getPathInfo() { 1175 return this.pathInfo; 1176 } 1177 1178 @Override 1179 @Nullable 1180 public String getPathTranslated() { 1181 return (this.pathInfo != null ? getRealPath(this.pathInfo) : null); 1182 } 1183 1184 public void setContextPath(String contextPath) { 1185 this.contextPath = contextPath; 1186 } 1187 1188 @Override 1189 public String getContextPath() { 1190 return this.contextPath; 1191 } 1192 1193 public void setQueryString(@Nullable String queryString) { 1194 this.queryString = queryString; 1195 } 1196 1197 @Override 1198 @Nullable 1199 public String getQueryString() { 1200 return this.queryString; 1201 } 1202 1203 public void setRemoteUser(@Nullable String remoteUser) { 1204 this.remoteUser = remoteUser; 1205 } 1206 1207 @Override 1208 @Nullable 1209 public String getRemoteUser() { 1210 return this.remoteUser; 1211 } 1212 1213 public void addUserRole(String role) { 1214 this.userRoles.add(role); 1215 } 1216 1217 @Override 1218 public boolean isUserInRole(String role) { 1219 return (this.userRoles.contains(role) || (this.servletContext instanceof MockServletContext && 1220 ((MockServletContext) this.servletContext).getDeclaredRoles().contains(role))); 1221 } 1222 1223 public void setUserPrincipal(@Nullable Principal userPrincipal) { 1224 this.userPrincipal = userPrincipal; 1225 } 1226 1227 @Override 1228 @Nullable 1229 public Principal getUserPrincipal() { 1230 return this.userPrincipal; 1231 } 1232 1233 public void setRequestedSessionId(@Nullable String requestedSessionId) { 1234 this.requestedSessionId = requestedSessionId; 1235 } 1236 1237 @Override 1238 @Nullable 1239 public String getRequestedSessionId() { 1240 return this.requestedSessionId; 1241 } 1242 1243 public void setRequestURI(@Nullable String requestURI) { 1244 this.requestURI = requestURI; 1245 } 1246 1247 @Override 1248 @Nullable 1249 public String getRequestURI() { 1250 return this.requestURI; 1251 } 1252 1253 @Override 1254 public StringBuffer getRequestURL() { 1255 String scheme = getScheme(); 1256 String server = getServerName(); 1257 int port = getServerPort(); 1258 String uri = getRequestURI(); 1259 1260 StringBuffer url = new StringBuffer(scheme).append("://").append(server); 1261 if (port > 0 && ((HTTP.equalsIgnoreCase(scheme) && port != 80) || 1262 (HTTPS.equalsIgnoreCase(scheme) && port != 443))) { 1263 url.append(':').append(port); 1264 } 1265 if (StringUtils.hasText(uri)) { 1266 url.append(uri); 1267 } 1268 return url; 1269 } 1270 1271 public void setServletPath(String servletPath) { 1272 this.servletPath = servletPath; 1273 } 1274 1275 @Override 1276 public String getServletPath() { 1277 return this.servletPath; 1278 } 1279 1280 public void setSession(HttpSession session) { 1281 this.session = session; 1282 if (session instanceof MockHttpSession) { 1283 MockHttpSession mockSession = ((MockHttpSession) session); 1284 mockSession.access(); 1285 } 1286 } 1287 1288 @Override 1289 @Nullable 1290 public HttpSession getSession(boolean create) { 1291 checkActive(); 1292 // Reset session if invalidated. 1293 if (this.session instanceof MockHttpSession && ((MockHttpSession) this.session).isInvalid()) { 1294 this.session = null; 1295 } 1296 // Create new session if necessary. 1297 if (this.session == null && create) { 1298 this.session = new MockHttpSession(this.servletContext); 1299 } 1300 return this.session; 1301 } 1302 1303 @Override 1304 @Nullable 1305 public HttpSession getSession() { 1306 return getSession(true); 1307 } 1308 1309 /** 1310 * The implementation of this (Servlet 3.1+) method calls 1311 * {@link MockHttpSession#changeSessionId()} if the session is a mock session. 1312 * Otherwise it simply returns the current session id. 1313 * @since 4.0.3 1314 */ 1315 @Override 1316 public String changeSessionId() { 1317 Assert.isTrue(this.session != null, "The request does not have a session"); 1318 if (this.session instanceof MockHttpSession) { 1319 return ((MockHttpSession) this.session).changeSessionId(); 1320 } 1321 return this.session.getId(); 1322 } 1323 1324 public void setRequestedSessionIdValid(boolean requestedSessionIdValid) { 1325 this.requestedSessionIdValid = requestedSessionIdValid; 1326 } 1327 1328 @Override 1329 public boolean isRequestedSessionIdValid() { 1330 return this.requestedSessionIdValid; 1331 } 1332 1333 public void setRequestedSessionIdFromCookie(boolean requestedSessionIdFromCookie) { 1334 this.requestedSessionIdFromCookie = requestedSessionIdFromCookie; 1335 } 1336 1337 @Override 1338 public boolean isRequestedSessionIdFromCookie() { 1339 return this.requestedSessionIdFromCookie; 1340 } 1341 1342 public void setRequestedSessionIdFromURL(boolean requestedSessionIdFromURL) { 1343 this.requestedSessionIdFromURL = requestedSessionIdFromURL; 1344 } 1345 1346 @Override 1347 public boolean isRequestedSessionIdFromURL() { 1348 return this.requestedSessionIdFromURL; 1349 } 1350 1351 @Override 1352 @Deprecated 1353 public boolean isRequestedSessionIdFromUrl() { 1354 return isRequestedSessionIdFromURL(); 1355 } 1356 1357 @Override 1358 public boolean authenticate(HttpServletResponse response) throws IOException, ServletException { 1359 throw new UnsupportedOperationException(); 1360 } 1361 1362 @Override 1363 public void login(String username, String password) throws ServletException { 1364 throw new UnsupportedOperationException(); 1365 } 1366 1367 @Override 1368 public void logout() throws ServletException { 1369 this.userPrincipal = null; 1370 this.remoteUser = null; 1371 this.authType = null; 1372 } 1373 1374 public void addPart(Part part) { 1375 this.parts.add(part.getName(), part); 1376 } 1377 1378 @Override 1379 @Nullable 1380 public Part getPart(String name) throws IOException, ServletException { 1381 return this.parts.getFirst(name); 1382 } 1383 1384 @Override 1385 public Collection<Part> getParts() throws IOException, ServletException { 1386 List<Part> result = new LinkedList<>(); 1387 for (List<Part> list : this.parts.values()) { 1388 result.addAll(list); 1389 } 1390 return result; 1391 } 1392 1393 @Override 1394 public <T extends HttpUpgradeHandler> T upgrade(Class<T> handlerClass) throws IOException, ServletException { 1395 throw new UnsupportedOperationException(); 1396 } 1397 1398}