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}