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.test.web.servlet.request;
018
019import java.io.ByteArrayInputStream;
020import java.io.IOException;
021import java.io.InputStream;
022import java.net.URI;
023import java.nio.charset.StandardCharsets;
024import java.security.Principal;
025import java.util.ArrayList;
026import java.util.Arrays;
027import java.util.LinkedHashMap;
028import java.util.List;
029import java.util.Locale;
030import java.util.Map;
031
032import javax.servlet.ServletContext;
033import javax.servlet.ServletRequest;
034import javax.servlet.http.Cookie;
035import javax.servlet.http.HttpSession;
036
037import org.springframework.beans.Mergeable;
038import org.springframework.beans.factory.NoSuchBeanDefinitionException;
039import org.springframework.http.HttpHeaders;
040import org.springframework.http.HttpInputMessage;
041import org.springframework.http.HttpMethod;
042import org.springframework.http.MediaType;
043import org.springframework.http.converter.FormHttpMessageConverter;
044import org.springframework.lang.Nullable;
045import org.springframework.mock.web.MockHttpServletRequest;
046import org.springframework.mock.web.MockHttpServletResponse;
047import org.springframework.mock.web.MockHttpSession;
048import org.springframework.test.web.servlet.MockMvc;
049import org.springframework.util.Assert;
050import org.springframework.util.LinkedMultiValueMap;
051import org.springframework.util.MultiValueMap;
052import org.springframework.util.ObjectUtils;
053import org.springframework.util.StreamUtils;
054import org.springframework.util.StringUtils;
055import org.springframework.web.context.WebApplicationContext;
056import org.springframework.web.context.support.WebApplicationContextUtils;
057import org.springframework.web.servlet.DispatcherServlet;
058import org.springframework.web.servlet.FlashMap;
059import org.springframework.web.servlet.FlashMapManager;
060import org.springframework.web.servlet.support.SessionFlashMapManager;
061import org.springframework.web.util.UriComponentsBuilder;
062import org.springframework.web.util.UriUtils;
063import org.springframework.web.util.UrlPathHelper;
064
065/**
066 * Default builder for {@link MockHttpServletRequest} required as input to
067 * perform requests in {@link MockMvc}.
068 *
069 * <p>Application tests will typically access this builder through the static
070 * factory methods in {@link MockMvcRequestBuilders}.
071 *
072 * <p>This class is not open for extension. To apply custom initialization to
073 * the created {@code MockHttpServletRequest}, please use the
074 * {@link #with(RequestPostProcessor)} extension point.
075 *
076 * @author Rossen Stoyanchev
077 * @author Juergen Hoeller
078 * @author Arjen Poutsma
079 * @author Sam Brannen
080 * @author Kamill Sokol
081 * @since 3.2
082 */
083public class MockHttpServletRequestBuilder
084                implements ConfigurableSmartRequestBuilder<MockHttpServletRequestBuilder>, Mergeable {
085
086        private final String method;
087
088        private final URI url;
089
090        private String contextPath = "";
091
092        private String servletPath = "";
093
094        @Nullable
095        private String pathInfo = "";
096
097        @Nullable
098        private Boolean secure;
099
100        @Nullable
101        private Principal principal;
102
103        @Nullable
104        private MockHttpSession session;
105
106        @Nullable
107        private String characterEncoding;
108
109        @Nullable
110        private byte[] content;
111
112        @Nullable
113        private String contentType;
114
115        private final MultiValueMap<String, Object> headers = new LinkedMultiValueMap<>();
116
117        private final MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
118
119        private final MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
120
121        private final List<Cookie> cookies = new ArrayList<>();
122
123        private final List<Locale> locales = new ArrayList<>();
124
125        private final Map<String, Object> requestAttributes = new LinkedHashMap<>();
126
127        private final Map<String, Object> sessionAttributes = new LinkedHashMap<>();
128
129        private final Map<String, Object> flashAttributes = new LinkedHashMap<>();
130
131        private final List<RequestPostProcessor> postProcessors = new ArrayList<>();
132
133
134        /**
135         * Package private constructor. To get an instance, use static factory
136         * methods in {@link MockMvcRequestBuilders}.
137         * <p>Although this class cannot be extended, additional ways to initialize
138         * the {@code MockHttpServletRequest} can be plugged in via
139         * {@link #with(RequestPostProcessor)}.
140         * @param httpMethod the HTTP method (GET, POST, etc)
141         * @param url a URL template; the resulting URL will be encoded
142         * @param vars zero or more URI variables
143         */
144        MockHttpServletRequestBuilder(HttpMethod httpMethod, String url, Object... vars) {
145                this(httpMethod.name(), initUri(url, vars));
146        }
147
148        private static URI initUri(String url, Object[] vars) {
149                Assert.notNull(url, "'url' must not be null");
150                Assert.isTrue(url.startsWith("/") || url.startsWith("http://") || url.startsWith("https://"), "" +
151                                "'url' should start with a path or be a complete HTTP URL: " + url);
152                return UriComponentsBuilder.fromUriString(url).buildAndExpand(vars).encode().toUri();
153        }
154
155        /**
156         * Alternative to {@link #MockHttpServletRequestBuilder(HttpMethod, String, Object...)}
157         * with a pre-built URI.
158         * @param httpMethod the HTTP method (GET, POST, etc)
159         * @param url the URL
160         * @since 4.0.3
161         */
162        MockHttpServletRequestBuilder(HttpMethod httpMethod, URI url) {
163                this(httpMethod.name(), url);
164        }
165
166        /**
167         * Alternative constructor for custom HTTP methods.
168         * @param httpMethod the HTTP method (GET, POST, etc)
169         * @param url the URL
170         * @since 4.3
171         */
172        MockHttpServletRequestBuilder(String httpMethod, URI url) {
173                Assert.notNull(httpMethod, "'httpMethod' is required");
174                Assert.notNull(url, "'url' is required");
175                this.method = httpMethod;
176                this.url = url;
177        }
178
179
180        /**
181         * Specify the portion of the requestURI that represents the context path.
182         * The context path, if specified, must match to the start of the request URI.
183         * <p>In most cases, tests can be written by omitting the context path from
184         * the requestURI. This is because most applications don't actually depend
185         * on the name under which they're deployed. If specified here, the context
186         * path must start with a "/" and must not end with a "/".
187         * @see javax.servlet.http.HttpServletRequest#getContextPath()
188         */
189        public MockHttpServletRequestBuilder contextPath(String contextPath) {
190                if (StringUtils.hasText(contextPath)) {
191                        Assert.isTrue(contextPath.startsWith("/"), "Context path must start with a '/'");
192                        Assert.isTrue(!contextPath.endsWith("/"), "Context path must not end with a '/'");
193                }
194                this.contextPath = contextPath;
195                return this;
196        }
197
198        /**
199         * Specify the portion of the requestURI that represents the path to which
200         * the Servlet is mapped. This is typically a portion of the requestURI
201         * after the context path.
202         * <p>In most cases, tests can be written by omitting the servlet path from
203         * the requestURI. This is because most applications don't actually depend
204         * on the prefix to which a servlet is mapped. For example if a Servlet is
205         * mapped to {@code "/main/*"}, tests can be written with the requestURI
206         * {@code "/accounts/1"} as opposed to {@code "/main/accounts/1"}.
207         * If specified here, the servletPath must start with a "/" and must not
208         * end with a "/".
209         * @see javax.servlet.http.HttpServletRequest#getServletPath()
210         */
211        public MockHttpServletRequestBuilder servletPath(String servletPath) {
212                if (StringUtils.hasText(servletPath)) {
213                        Assert.isTrue(servletPath.startsWith("/"), "Servlet path must start with a '/'");
214                        Assert.isTrue(!servletPath.endsWith("/"), "Servlet path must not end with a '/'");
215                }
216                this.servletPath = servletPath;
217                return this;
218        }
219
220        /**
221         * Specify the portion of the requestURI that represents the pathInfo.
222         * <p>If left unspecified (recommended), the pathInfo will be automatically derived
223         * by removing the contextPath and the servletPath from the requestURI and using any
224         * remaining part. If specified here, the pathInfo must start with a "/".
225         * <p>If specified, the pathInfo will be used as-is.
226         * @see javax.servlet.http.HttpServletRequest#getPathInfo()
227         */
228        public MockHttpServletRequestBuilder pathInfo(@Nullable String pathInfo) {
229                if (StringUtils.hasText(pathInfo)) {
230                        Assert.isTrue(pathInfo.startsWith("/"), "Path info must start with a '/'");
231                }
232                this.pathInfo = pathInfo;
233                return this;
234        }
235
236        /**
237         * Set the secure property of the {@link ServletRequest} indicating use of a
238         * secure channel, such as HTTPS.
239         * @param secure whether the request is using a secure channel
240         */
241        public MockHttpServletRequestBuilder secure(boolean secure){
242                this.secure = secure;
243                return this;
244        }
245
246        /**
247         * Set the character encoding of the request.
248         * @param encoding the character encoding
249         */
250        public MockHttpServletRequestBuilder characterEncoding(String encoding) {
251                this.characterEncoding = encoding;
252                return this;
253        }
254
255        /**
256         * Set the request body.
257         * <p>If content is provided and {@link #contentType(MediaType)} is set to
258         * {@code application/x-www-form-urlencoded}, the content will be parsed
259         * and used to populate the {@link #param(String, String...) request
260         * parameters} map.
261         * @param content the body content
262         */
263        public MockHttpServletRequestBuilder content(byte[] content) {
264                this.content = content;
265                return this;
266        }
267
268        /**
269         * Set the request body as a UTF-8 String.
270         * <p>If content is provided and {@link #contentType(MediaType)} is set to
271         * {@code application/x-www-form-urlencoded}, the content will be parsed
272         * and used to populate the {@link #param(String, String...) request
273         * parameters} map.
274         * @param content the body content
275         */
276        public MockHttpServletRequestBuilder content(String content) {
277                this.content = content.getBytes(StandardCharsets.UTF_8);
278                return this;
279        }
280
281        /**
282         * Set the 'Content-Type' header of the request.
283         * <p>If content is provided and {@code contentType} is set to
284         * {@code application/x-www-form-urlencoded}, the content will be parsed
285         * and used to populate the {@link #param(String, String...) request
286         * parameters} map.
287         * @param contentType the content type
288         */
289        public MockHttpServletRequestBuilder contentType(MediaType contentType) {
290                Assert.notNull(contentType, "'contentType' must not be null");
291                this.contentType = contentType.toString();
292                return this;
293        }
294
295        /**
296         * Set the 'Content-Type' header of the request as a raw String value,
297         * possibly not even well formed (for testing purposes).
298         * @param contentType the content type
299         * @since 4.1.2
300         */
301        public MockHttpServletRequestBuilder contentType(String contentType) {
302                Assert.notNull(contentType, "'contentType' must not be null");
303                this.contentType = contentType;
304                return this;
305        }
306
307        /**
308         * Set the 'Accept' header to the given media type(s).
309         * @param mediaTypes one or more media types
310         */
311        public MockHttpServletRequestBuilder accept(MediaType... mediaTypes) {
312                Assert.notEmpty(mediaTypes, "'mediaTypes' must not be empty");
313                this.headers.set("Accept", MediaType.toString(Arrays.asList(mediaTypes)));
314                return this;
315        }
316
317        /**
318         * Set the 'Accept' header using raw String values, possibly not even well
319         * formed (for testing purposes).
320         * @param mediaTypes one or more media types; internally joined as
321         * comma-separated String
322         */
323        public MockHttpServletRequestBuilder accept(String... mediaTypes) {
324                Assert.notEmpty(mediaTypes, "'mediaTypes' must not be empty");
325                this.headers.set("Accept", String.join(", ", mediaTypes));
326                return this;
327        }
328
329        /**
330         * Add a header to the request. Values are always added.
331         * @param name the header name
332         * @param values one or more header values
333         */
334        public MockHttpServletRequestBuilder header(String name, Object... values) {
335                addToMultiValueMap(this.headers, name, values);
336                return this;
337        }
338
339        /**
340         * Add all headers to the request. Values are always added.
341         * @param httpHeaders the headers and values to add
342         */
343        public MockHttpServletRequestBuilder headers(HttpHeaders httpHeaders) {
344                httpHeaders.forEach(this.headers::addAll);
345                return this;
346        }
347
348        /**
349         * Add a request parameter to {@link MockHttpServletRequest#getParameterMap()}.
350         * <p>In the Servlet API, a request parameter may be parsed from the query
351         * string and/or from the body of an {@code application/x-www-form-urlencoded}
352         * request. This method simply adds to the request parameter map. You may
353         * also use add Servlet request parameters by specifying the query or form
354         * data through one of the following:
355         * <ul>
356         * <li>Supply a URL with a query to {@link MockMvcRequestBuilders}.
357         * <li>Add query params via {@link #queryParam} or {@link #queryParams}.
358         * <li>Provide {@link #content} with {@link #contentType}
359         * {@code application/x-www-form-urlencoded}.
360         * </ul>
361         * @param name the parameter name
362         * @param values one or more values
363         */
364        public MockHttpServletRequestBuilder param(String name, String... values) {
365                addToMultiValueMap(this.parameters, name, values);
366                return this;
367        }
368
369        /**
370         * Variant of {@link #param(String, String...)} with a {@link MultiValueMap}.
371         * @param params the parameters to add
372         * @since 4.2.4
373         */
374        public MockHttpServletRequestBuilder params(MultiValueMap<String, String> params) {
375                params.forEach((name, values) -> {
376                        for (String value : values) {
377                                this.parameters.add(name, value);
378                        }
379                });
380                return this;
381        }
382
383        /**
384         * Append to the query string and also add to the
385         * {@link #param(String, String...) request parameters} map. The parameter
386         * name and value are encoded when they are added to the query string.
387         * @param name the parameter name
388         * @param values one or more values
389         * @since 5.2.2
390         */
391        public MockHttpServletRequestBuilder queryParam(String name, String... values) {
392                param(name, values);
393                this.queryParams.addAll(name, Arrays.asList(values));
394                return this;
395        }
396
397        /**
398         * Append to the query string and also add to the
399         * {@link #params(MultiValueMap)}  request parameters} map. The parameter
400         * name and value are encoded when they are added to the query string.
401         * @param params the parameters to add
402         * @since 5.2.2
403         */
404        public MockHttpServletRequestBuilder queryParams(MultiValueMap<String, String> params) {
405                params(params);
406                this.queryParams.addAll(params);
407                return this;
408        }
409
410        /**
411         * Add the given cookies to the request. Cookies are always added.
412         * @param cookies the cookies to add
413         */
414        public MockHttpServletRequestBuilder cookie(Cookie... cookies) {
415                Assert.notEmpty(cookies, "'cookies' must not be empty");
416                this.cookies.addAll(Arrays.asList(cookies));
417                return this;
418        }
419
420        /**
421         * Add the specified locales as preferred request locales.
422         * @param locales the locales to add
423         * @since 4.3.6
424         * @see #locale(Locale)
425         */
426        public MockHttpServletRequestBuilder locale(Locale... locales) {
427                Assert.notEmpty(locales, "'locales' must not be empty");
428                this.locales.addAll(Arrays.asList(locales));
429                return this;
430        }
431
432        /**
433         * Set the locale of the request, overriding any previous locales.
434         * @param locale the locale, or {@code null} to reset it
435         * @see #locale(Locale...)
436         */
437        public MockHttpServletRequestBuilder locale(@Nullable Locale locale) {
438                this.locales.clear();
439                if (locale != null) {
440                        this.locales.add(locale);
441                }
442                return this;
443        }
444
445        /**
446         * Set a request attribute.
447         * @param name the attribute name
448         * @param value the attribute value
449         */
450        public MockHttpServletRequestBuilder requestAttr(String name, Object value) {
451                addToMap(this.requestAttributes, name, value);
452                return this;
453        }
454
455        /**
456         * Set a session attribute.
457         * @param name the session attribute name
458         * @param value the session attribute value
459         */
460        public MockHttpServletRequestBuilder sessionAttr(String name, Object value) {
461                addToMap(this.sessionAttributes, name, value);
462                return this;
463        }
464
465        /**
466         * Set session attributes.
467         * @param sessionAttributes the session attributes
468         */
469        public MockHttpServletRequestBuilder sessionAttrs(Map<String, Object> sessionAttributes) {
470                Assert.notEmpty(sessionAttributes, "'sessionAttributes' must not be empty");
471                sessionAttributes.forEach(this::sessionAttr);
472                return this;
473        }
474
475        /**
476         * Set an "input" flash attribute.
477         * @param name the flash attribute name
478         * @param value the flash attribute value
479         */
480        public MockHttpServletRequestBuilder flashAttr(String name, Object value) {
481                addToMap(this.flashAttributes, name, value);
482                return this;
483        }
484
485        /**
486         * Set flash attributes.
487         * @param flashAttributes the flash attributes
488         */
489        public MockHttpServletRequestBuilder flashAttrs(Map<String, Object> flashAttributes) {
490                Assert.notEmpty(flashAttributes, "'flashAttributes' must not be empty");
491                flashAttributes.forEach(this::flashAttr);
492                return this;
493        }
494
495        /**
496         * Set the HTTP session to use, possibly re-used across requests.
497         * <p>Individual attributes provided via {@link #sessionAttr(String, Object)}
498         * override the content of the session provided here.
499         * @param session the HTTP session
500         */
501        public MockHttpServletRequestBuilder session(MockHttpSession session) {
502                Assert.notNull(session, "'session' must not be null");
503                this.session = session;
504                return this;
505        }
506
507        /**
508         * Set the principal of the request.
509         * @param principal the principal
510         */
511        public MockHttpServletRequestBuilder principal(Principal principal) {
512                Assert.notNull(principal, "'principal' must not be null");
513                this.principal = principal;
514                return this;
515        }
516
517        /**
518         * An extension point for further initialization of {@link MockHttpServletRequest}
519         * in ways not built directly into the {@code MockHttpServletRequestBuilder}.
520         * Implementation of this interface can have builder-style methods themselves
521         * and be made accessible through static factory methods.
522         * @param postProcessor a post-processor to add
523         */
524        @Override
525        public MockHttpServletRequestBuilder with(RequestPostProcessor postProcessor) {
526                Assert.notNull(postProcessor, "postProcessor is required");
527                this.postProcessors.add(postProcessor);
528                return this;
529        }
530
531
532        /**
533         * {@inheritDoc}
534         * @return always returns {@code true}.
535         */
536        @Override
537        public boolean isMergeEnabled() {
538                return true;
539        }
540
541        /**
542         * Merges the properties of the "parent" RequestBuilder accepting values
543         * only if not already set in "this" instance.
544         * @param parent the parent {@code RequestBuilder} to inherit properties from
545         * @return the result of the merge
546         */
547        @Override
548        public Object merge(@Nullable Object parent) {
549                if (parent == null) {
550                        return this;
551                }
552                if (!(parent instanceof MockHttpServletRequestBuilder)) {
553                        throw new IllegalArgumentException("Cannot merge with [" + parent.getClass().getName() + "]");
554                }
555                MockHttpServletRequestBuilder parentBuilder = (MockHttpServletRequestBuilder) parent;
556
557                if (!StringUtils.hasText(this.contextPath)) {
558                        this.contextPath = parentBuilder.contextPath;
559                }
560                if (!StringUtils.hasText(this.servletPath)) {
561                        this.servletPath = parentBuilder.servletPath;
562                }
563                if ("".equals(this.pathInfo)) {
564                        this.pathInfo = parentBuilder.pathInfo;
565                }
566
567                if (this.secure == null) {
568                        this.secure = parentBuilder.secure;
569                }
570                if (this.principal == null) {
571                        this.principal = parentBuilder.principal;
572                }
573                if (this.session == null) {
574                        this.session = parentBuilder.session;
575                }
576
577                if (this.characterEncoding == null) {
578                        this.characterEncoding = parentBuilder.characterEncoding;
579                }
580                if (this.content == null) {
581                        this.content = parentBuilder.content;
582                }
583                if (this.contentType == null) {
584                        this.contentType = parentBuilder.contentType;
585                }
586
587                for (Map.Entry<String, List<Object>> entry : parentBuilder.headers.entrySet()) {
588                        String headerName = entry.getKey();
589                        if (!this.headers.containsKey(headerName)) {
590                                this.headers.put(headerName, entry.getValue());
591                        }
592                }
593                for (Map.Entry<String, List<String>> entry : parentBuilder.parameters.entrySet()) {
594                        String paramName = entry.getKey();
595                        if (!this.parameters.containsKey(paramName)) {
596                                this.parameters.put(paramName, entry.getValue());
597                        }
598                }
599                for (Map.Entry<String, List<String>> entry : parentBuilder.queryParams.entrySet()) {
600                        String paramName = entry.getKey();
601                        if (!this.queryParams.containsKey(paramName)) {
602                                this.queryParams.put(paramName, entry.getValue());
603                        }
604                }
605                for (Cookie cookie : parentBuilder.cookies) {
606                        if (!containsCookie(cookie)) {
607                                this.cookies.add(cookie);
608                        }
609                }
610                for (Locale locale : parentBuilder.locales) {
611                        if (!this.locales.contains(locale)) {
612                                this.locales.add(locale);
613                        }
614                }
615
616                for (Map.Entry<String, Object> entry : parentBuilder.requestAttributes.entrySet()) {
617                        String attributeName = entry.getKey();
618                        if (!this.requestAttributes.containsKey(attributeName)) {
619                                this.requestAttributes.put(attributeName, entry.getValue());
620                        }
621                }
622                for (Map.Entry<String, Object> entry : parentBuilder.sessionAttributes.entrySet()) {
623                        String attributeName = entry.getKey();
624                        if (!this.sessionAttributes.containsKey(attributeName)) {
625                                this.sessionAttributes.put(attributeName, entry.getValue());
626                        }
627                }
628                for (Map.Entry<String, Object> entry : parentBuilder.flashAttributes.entrySet()) {
629                        String attributeName = entry.getKey();
630                        if (!this.flashAttributes.containsKey(attributeName)) {
631                                this.flashAttributes.put(attributeName, entry.getValue());
632                        }
633                }
634
635                this.postProcessors.addAll(0, parentBuilder.postProcessors);
636
637                return this;
638        }
639
640        private boolean containsCookie(Cookie cookie) {
641                for (Cookie cookieToCheck : this.cookies) {
642                        if (ObjectUtils.nullSafeEquals(cookieToCheck.getName(), cookie.getName())) {
643                                return true;
644                        }
645                }
646                return false;
647        }
648
649        /**
650         * Build a {@link MockHttpServletRequest}.
651         */
652        @Override
653        public final MockHttpServletRequest buildRequest(ServletContext servletContext) {
654                MockHttpServletRequest request = createServletRequest(servletContext);
655
656                request.setAsyncSupported(true);
657                request.setMethod(this.method);
658
659                String requestUri = this.url.getRawPath();
660                request.setRequestURI(requestUri);
661
662                if (this.url.getScheme() != null) {
663                        request.setScheme(this.url.getScheme());
664                }
665                if (this.url.getHost() != null) {
666                        request.setServerName(this.url.getHost());
667                }
668                if (this.url.getPort() != -1) {
669                        request.setServerPort(this.url.getPort());
670                }
671
672                updatePathRequestProperties(request, requestUri);
673
674                if (this.secure != null) {
675                        request.setSecure(this.secure);
676                }
677                if (this.principal != null) {
678                        request.setUserPrincipal(this.principal);
679                }
680                if (this.session != null) {
681                        request.setSession(this.session);
682                }
683
684                request.setCharacterEncoding(this.characterEncoding);
685                request.setContent(this.content);
686                request.setContentType(this.contentType);
687
688                this.headers.forEach((name, values) -> {
689                        for (Object value : values) {
690                                request.addHeader(name, value);
691                        }
692                });
693
694                if (!ObjectUtils.isEmpty(this.content) &&
695                                !this.headers.containsKey(HttpHeaders.CONTENT_LENGTH) &&
696                                !this.headers.containsKey(HttpHeaders.TRANSFER_ENCODING)) {
697
698                        request.addHeader(HttpHeaders.CONTENT_LENGTH, this.content.length);
699                }
700
701                String query = this.url.getRawQuery();
702                if (!this.queryParams.isEmpty()) {
703                        String str = UriComponentsBuilder.newInstance().queryParams(this.queryParams).build().encode().getQuery();
704                        query = StringUtils.hasLength(query) ? (query + "&" + str) : str;
705                }
706                if (query != null) {
707                        request.setQueryString(query);
708                }
709                addRequestParams(request, UriComponentsBuilder.fromUri(this.url).build().getQueryParams());
710
711                this.parameters.forEach((name, values) -> {
712                        for (String value : values) {
713                                request.addParameter(name, value);
714                        }
715                });
716
717                if (this.content != null && this.content.length > 0) {
718                        String requestContentType = request.getContentType();
719                        if (requestContentType != null) {
720                                try {
721                                        MediaType mediaType = MediaType.parseMediaType(requestContentType);
722                                        if (MediaType.APPLICATION_FORM_URLENCODED.includes(mediaType)) {
723                                                addRequestParams(request, parseFormData(mediaType));
724                                        }
725                                }
726                                catch (Exception ex) {
727                                        // Must be invalid, ignore..
728                                }
729                        }
730                }
731
732                if (!ObjectUtils.isEmpty(this.cookies)) {
733                        request.setCookies(this.cookies.toArray(new Cookie[0]));
734                }
735                if (!ObjectUtils.isEmpty(this.locales)) {
736                        request.setPreferredLocales(this.locales);
737                }
738
739                this.requestAttributes.forEach(request::setAttribute);
740                this.sessionAttributes.forEach((name, attribute) -> {
741                        HttpSession session = request.getSession();
742                        Assert.state(session != null, "No HttpSession");
743                        session.setAttribute(name, attribute);
744                });
745
746                FlashMap flashMap = new FlashMap();
747                flashMap.putAll(this.flashAttributes);
748                FlashMapManager flashMapManager = getFlashMapManager(request);
749                flashMapManager.saveOutputFlashMap(flashMap, request, new MockHttpServletResponse());
750
751                return request;
752        }
753
754        /**
755         * Create a new {@link MockHttpServletRequest} based on the supplied
756         * {@code ServletContext}.
757         * <p>Can be overridden in subclasses.
758         */
759        protected MockHttpServletRequest createServletRequest(ServletContext servletContext) {
760                return new MockHttpServletRequest(servletContext);
761        }
762
763        /**
764         * Update the contextPath, servletPath, and pathInfo of the request.
765         */
766        private void updatePathRequestProperties(MockHttpServletRequest request, String requestUri) {
767                if (!requestUri.startsWith(this.contextPath)) {
768                        throw new IllegalArgumentException(
769                                        "Request URI [" + requestUri + "] does not start with context path [" + this.contextPath + "]");
770                }
771                request.setContextPath(this.contextPath);
772                request.setServletPath(this.servletPath);
773
774                if ("".equals(this.pathInfo)) {
775                        if (!requestUri.startsWith(this.contextPath + this.servletPath)) {
776                                throw new IllegalArgumentException(
777                                                "Invalid servlet path [" + this.servletPath + "] for request URI [" + requestUri + "]");
778                        }
779                        String extraPath = requestUri.substring(this.contextPath.length() + this.servletPath.length());
780                        this.pathInfo = (StringUtils.hasText(extraPath) ?
781                                        UrlPathHelper.defaultInstance.decodeRequestString(request, extraPath) : null);
782                }
783                request.setPathInfo(this.pathInfo);
784        }
785
786        private void addRequestParams(MockHttpServletRequest request, MultiValueMap<String, String> map) {
787                map.forEach((key, values) -> values.forEach(value -> {
788                        value = (value != null ? UriUtils.decode(value, StandardCharsets.UTF_8) : null);
789                        request.addParameter(UriUtils.decode(key, StandardCharsets.UTF_8), value);
790                }));
791        }
792
793        private MultiValueMap<String, String> parseFormData(MediaType mediaType) {
794                HttpInputMessage message = new HttpInputMessage() {
795                        @Override
796                        public InputStream getBody() {
797                                return (content != null ? new ByteArrayInputStream(content) : StreamUtils.emptyInput());
798                        }
799                        @Override
800                        public HttpHeaders getHeaders() {
801                                HttpHeaders headers = new HttpHeaders();
802                                headers.setContentType(mediaType);
803                                return headers;
804                        }
805                };
806
807                try {
808                        return new FormHttpMessageConverter().read(null, message);
809                }
810                catch (IOException ex) {
811                        throw new IllegalStateException("Failed to parse form data in request body", ex);
812                }
813        }
814
815        private FlashMapManager getFlashMapManager(MockHttpServletRequest request) {
816                FlashMapManager flashMapManager = null;
817                try {
818                        ServletContext servletContext = request.getServletContext();
819                        WebApplicationContext wac = WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);
820                        flashMapManager = wac.getBean(DispatcherServlet.FLASH_MAP_MANAGER_BEAN_NAME, FlashMapManager.class);
821                }
822                catch (IllegalStateException | NoSuchBeanDefinitionException ex) {
823                        // ignore
824                }
825                return (flashMapManager != null ? flashMapManager : new SessionFlashMapManager());
826        }
827
828        @Override
829        public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
830                for (RequestPostProcessor postProcessor : this.postProcessors) {
831                        request = postProcessor.postProcessRequest(request);
832                }
833                return request;
834        }
835
836
837        private static void addToMap(Map<String, Object> map, String name, Object value) {
838                Assert.hasLength(name, "'name' must not be empty");
839                Assert.notNull(value, "'value' must not be null");
840                map.put(name, value);
841        }
842
843        private static <T> void addToMultiValueMap(MultiValueMap<String, T> map, String name, T[] values) {
844                Assert.hasLength(name, "'name' must not be empty");
845                Assert.notEmpty(values, "'values' must not be empty");
846                for (T value : values) {
847                        map.add(name, value);
848                }
849        }
850
851}