diff --git a/build.gradle b/build.gradle
index aeae50994f7..216662d3f2c 100644
--- a/build.gradle
+++ b/build.gradle
@@ -44,6 +44,7 @@ configure(allprojects) { project ->
ext.httpclientVersion = "4.5"
ext.httpasyncVersion = "4.1"
ext.jackson2Version = "2.6.0"
+ ext.htmlunitVersion = "2.18"
ext.jasperreportsVersion = "6.1.0"
ext.javamailVersion = "1.5.4"
ext.jettyVersion = "9.3.1.v20150714"
@@ -56,6 +57,7 @@ configure(allprojects) { project ->
ext.poiVersion = "3.12"
ext.protobufVersion = "2.6.1"
ext.reactorVersion = "2.0.4.RELEASE"
+ ext.seleniumVersion = "2.46.0"
ext.slf4jVersion = "1.7.12"
ext.snakeyamlVersion = "1.15"
ext.snifferVersion = "1.14"
@@ -1009,6 +1011,8 @@ project("spring-test") {
optional("com.jayway.jsonpath:json-path:2.0.0")
optional("org.skyscreamer:jsonassert:1.2.3")
optional("xmlunit:xmlunit:${xmlunitVersion}")
+ optional("net.sourceforge.htmlunit:htmlunit:$htmlunitVersion")
+ optional("org.seleniumhq.selenium:selenium-htmlunit-driver:$seleniumVersion")
testCompile(project(":spring-context-support"))
testCompile(project(":spring-oxm"))
testCompile("javax.mail:javax.mail-api:${javamailVersion}")
@@ -1028,6 +1032,7 @@ project("spring-test") {
}
testCompile("org.hsqldb:hsqldb:${hsqldbVersion}")
testCompile("org.slf4j:slf4j-jcl:${slf4jVersion}")
+ testCompile("org.apache.httpcomponents:httpclient:$httpclientVersion")
}
task testNG(type: Test) {
diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/DelegatingWebConnection.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/DelegatingWebConnection.java
new file mode 100644
index 00000000000..1fc2e176709
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/DelegatingWebConnection.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2002-2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.springframework.test.web.servlet.htmlunit;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+
+import org.springframework.util.Assert;
+
+import com.gargoylesoftware.htmlunit.WebConnection;
+import com.gargoylesoftware.htmlunit.WebRequest;
+import com.gargoylesoftware.htmlunit.WebResponse;
+
+/**
+ * Implementation of {@link WebConnection} that allows delegating to various
+ * {@code WebConnection} implementations.
+ *
+ *
For example, if you host your JavaScript on the domain {@code code.jquery.com},
+ * you might want to use the following.
+ *
+ *
+ * WebClient webClient = new WebClient();
+ *
+ * MockMvc mockMvc = ...
+ * MockMvcWebConnection mockConnection = new MockMvcWebConnection(mockMvc);
+ * mockConnection.setWebClient(webClient);
+ *
+ * WebRequestMatcher cdnMatcher = new UrlRegexRequestMatcher(".*?//code.jquery.com/.*");
+ * WebConnection httpConnection = new HttpWebConnection(webClient);
+ * WebConnection webConnection = new DelegatingWebConnection(mockConnection, new DelegateWebConnection(cdnMatcher, httpConnection));
+ *
+ * webClient.setWebConnection(webConnection);
+ *
+ * WebClient webClient = new WebClient();
+ * webClient.setWebConnection(webConnection);
+ *
+ *
+ * @author Rob Winch
+ * @author Sam Brannen
+ * @since 4.2
+ */
+public final class DelegatingWebConnection implements WebConnection {
+
+ private final List connections;
+
+ private final WebConnection defaultConnection;
+
+
+ public DelegatingWebConnection(WebConnection defaultConnection, List connections) {
+ Assert.notNull(defaultConnection, "defaultConnection must not be null");
+ Assert.notEmpty(connections, "connections must not be empty");
+ this.connections = connections;
+ this.defaultConnection = defaultConnection;
+ }
+
+ public DelegatingWebConnection(WebConnection defaultConnection, DelegateWebConnection... connections) {
+ this(defaultConnection, Arrays.asList(connections));
+ }
+
+ @Override
+ public WebResponse getResponse(WebRequest request) throws IOException {
+ for (DelegateWebConnection connection : this.connections) {
+ if (connection.getMatcher().matches(request)) {
+ return connection.getDelegate().getResponse(request);
+ }
+ }
+ return this.defaultConnection.getResponse(request);
+ }
+
+
+ public static final class DelegateWebConnection {
+
+ private final WebRequestMatcher matcher;
+
+ private final WebConnection delegate;
+
+
+ public DelegateWebConnection(WebRequestMatcher matcher, WebConnection delegate) {
+ this.matcher = matcher;
+ this.delegate = delegate;
+ }
+
+ private WebRequestMatcher getMatcher() {
+ return matcher;
+ }
+
+ private WebConnection getDelegate() {
+ return delegate;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/ForwardRequestPostProcessor.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/ForwardRequestPostProcessor.java
new file mode 100644
index 00000000000..75284bf4475
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/ForwardRequestPostProcessor.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2002-2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.springframework.test.web.servlet.htmlunit;
+
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.test.web.servlet.request.RequestPostProcessor;
+import org.springframework.util.Assert;
+
+/**
+ * @author Rob Winch
+ * @author Sam Brannen
+ * @since 4.2
+ */
+final class ForwardRequestPostProcessor implements RequestPostProcessor {
+
+ private final String forwardUrl;
+
+
+ public ForwardRequestPostProcessor(String forwardUrl) {
+ Assert.hasText(forwardUrl, "forwardUrl must not be null or empty");
+ this.forwardUrl = forwardUrl;
+ }
+
+ @Override
+ public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
+ request.setServletPath(this.forwardUrl);
+ return request;
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HostRequestMatcher.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HostRequestMatcher.java
new file mode 100644
index 00000000000..e6cee1e665a
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HostRequestMatcher.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2002-2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.springframework.test.web.servlet.htmlunit;
+
+import java.net.URL;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import com.gargoylesoftware.htmlunit.WebRequest;
+
+/**
+ * A {@link WebRequestMatcher} that allows matching on the host and optionally
+ * the port of {@code WebRequest#getUrl()}.
+ *
+ *
For example, the following would match any request to the host
+ * {@code "code.jquery.com"} without regard for the port.
+ *
+ *
WebRequestMatcher cdnMatcher = new HostMatcher("code.jquery.com");
+ *
+ *
Multiple hosts can also be passed in. For example, the following would
+ * match any request to the host {@code "code.jquery.com"} or the host
+ * {@code "cdn.com"} without regard for the port.
+ *
+ *
WebRequestMatcher cdnMatcher = new HostMatcher("code.jquery.com", "cdn.com");
+ *
+ *
Alternatively, one can also specify the port. For example, the following would match
+ * any request to the host {@code "code.jquery.com"} with the port of {@code 80}.
+ *
+ *
WebRequestMatcher cdnMatcher = new HostMatcher("code.jquery.com:80");
+ *
+ *
The above {@code cdnMatcher} would match {@code "http://code.jquery.com/jquery.js"}
+ * which has a default port of {@code 80} and {@code "http://code.jquery.com:80/jquery.js"}.
+ * However, it would not match {@code "https://code.jquery.com/jquery.js"}
+ * which has a default port of {@code 443}.
+ *
+ * @author Rob Winch
+ * @author Sam Brannen
+ * @since 4.2
+ * @see UrlRegexRequestMatcher
+ * @see org.springframework.test.web.servlet.htmlunit.DelegatingWebConnection
+ */
+public final class HostRequestMatcher implements WebRequestMatcher {
+
+ private final Set hosts = new HashSet();
+
+
+ /**
+ * Create a new {@code HostRequestMatcher} for the given hosts —
+ * for example: {@code "localhost"}, {@code "example.com:443"}, etc.
+ * @param hosts the hosts to match on
+ */
+ public HostRequestMatcher(String... hosts) {
+ this.hosts.addAll(Arrays.asList(hosts));
+ }
+
+ @Override
+ public boolean matches(WebRequest request) {
+ URL url = request.getUrl();
+ String host = url.getHost();
+
+ if (this.hosts.contains(host)) {
+ return true;
+ }
+
+ int port = url.getPort();
+ if (port == -1) {
+ port = url.getDefaultPort();
+ }
+ String hostAndPort = host + ":" + port;
+
+ return this.hosts.contains(hostAndPort);
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java
new file mode 100644
index 00000000000..d9ef067c6ce
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java
@@ -0,0 +1,516 @@
+/*
+ * Copyright 2002-2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.springframework.test.web.servlet.htmlunit;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URL;
+import java.net.URLDecoder;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.servlet.ServletContext;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+
+import org.springframework.beans.Mergeable;
+import org.springframework.http.MediaType;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpSession;
+import org.springframework.test.web.servlet.RequestBuilder;
+import org.springframework.test.web.servlet.SmartRequestBuilder;
+import org.springframework.test.web.servlet.request.RequestPostProcessor;
+import org.springframework.util.Assert;
+import org.springframework.web.util.UriComponents;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import com.gargoylesoftware.htmlunit.CookieManager;
+import com.gargoylesoftware.htmlunit.WebClient;
+import com.gargoylesoftware.htmlunit.WebRequest;
+import com.gargoylesoftware.htmlunit.util.NameValuePair;
+
+/**
+ * Internal class used to transform a {@link WebRequest} into a
+ * {@link MockHttpServletRequest} using Spring MVC Test's {@link RequestBuilder}.
+ *
+ *
By default the first path segment of the URL is used as the contextPath.
+ * To override this default see {@link #setContextPath(String)}.
+ *
+ * @author Rob Winch
+ * @author Sam Brannen
+ * @since 4.2
+ * @see MockMvcWebConnection
+ */
+final class HtmlUnitRequestBuilder implements RequestBuilder, Mergeable {
+
+ private static final Pattern LOCALE_PATTERN = Pattern.compile("^\\s*(\\w{2})(?:-(\\w{2}))?(?:;q=(\\d+\\.\\d+))?$");
+
+ private final Map sessions;
+
+ private final WebClient webClient;
+
+ private final WebRequest webRequest;
+
+ private String contextPath;
+
+ private RequestBuilder parentBuilder;
+
+ private SmartRequestBuilder parentPostProcessor;
+
+ private RequestPostProcessor forwardPostProcessor;
+
+
+ /**
+ * Construct a new {@code HtmlUnitRequestBuilder}.
+ * @param sessions a {@link Map} from session {@linkplain HttpSession#getId() IDs}
+ * to currently managed {@link HttpSession} objects; never {@code null}
+ * @param webClient the WebClient for retrieving cookies
+ * @param webRequest the {@link WebRequest} to transform into a
+ * {@link MockHttpServletRequest}; never {@code null}
+ */
+ public HtmlUnitRequestBuilder(Map sessions, WebClient webClient, WebRequest webRequest) {
+ Assert.notNull(sessions, "sessions map must not be null");
+ Assert.notNull(webClient, "webClient must not be null");
+ Assert.notNull(webRequest, "webRequest must not be null");
+
+ this.sessions = sessions;
+ this.webClient = webClient;
+ this.webRequest = webRequest;
+ }
+
+ public MockHttpServletRequest buildRequest(ServletContext servletContext) {
+ String charset = getCharset();
+ String httpMethod = this.webRequest.getHttpMethod().name();
+ UriComponents uriComponents = uriComponents();
+
+ MockHttpServletRequest result = new HtmlUnitMockHttpServletRequest(servletContext, httpMethod,
+ uriComponents.getPath());
+ parent(result, this.parentBuilder);
+ result.setServerName(uriComponents.getHost()); // needs to be first for additional headers
+ authType(result);
+ result.setCharacterEncoding(charset);
+ content(result, charset);
+ contextPath(result, uriComponents);
+ contentType(result);
+ cookies(result);
+ headers(result);
+ locales(result);
+ servletPath(uriComponents, result);
+ params(result, uriComponents);
+ ports(uriComponents, result);
+ result.setProtocol("HTTP/1.1");
+ result.setQueryString(uriComponents.getQuery());
+ result.setScheme(uriComponents.getScheme());
+ pathInfo(uriComponents,result);
+
+ return postProcess(result);
+ }
+
+ private MockHttpServletRequest postProcess(MockHttpServletRequest request) {
+ if (this.parentPostProcessor != null) {
+ request = this.parentPostProcessor.postProcessRequest(request);
+ }
+ if (this.forwardPostProcessor != null) {
+ request = this.forwardPostProcessor.postProcessRequest(request);
+ }
+ return request;
+ }
+
+ private void parent(MockHttpServletRequest result, RequestBuilder parent) {
+ if (parent == null) {
+ return;
+ }
+
+ MockHttpServletRequest parentRequest = parent.buildRequest(result.getServletContext());
+
+ // session
+ HttpSession parentSession = parentRequest.getSession(false);
+ if (parentSession != null) {
+ Enumeration attrNames = parentSession.getAttributeNames();
+ while (attrNames.hasMoreElements()) {
+ String attrName = attrNames.nextElement();
+ Object attrValue = parentSession.getAttribute(attrName);
+ result.getSession().setAttribute(attrName, attrValue);
+ }
+ }
+
+ // header
+ Enumeration headerNames = parentRequest.getHeaderNames();
+ while (headerNames.hasMoreElements()) {
+ String attrName = headerNames.nextElement();
+ Enumeration attrValues = parentRequest.getHeaders(attrName);
+ while (attrValues.hasMoreElements()) {
+ String attrValue = attrValues.nextElement();
+ result.addHeader(attrName, attrValue);
+ }
+ }
+
+ // parameter
+ Map parentParams = parentRequest.getParameterMap();
+ for (Map.Entry parentParam : parentParams.entrySet()) {
+ String paramName = parentParam.getKey();
+ String[] paramValues = parentParam.getValue();
+ result.addParameter(paramName, paramValues);
+ }
+
+ // cookie
+ Cookie[] parentCookies = parentRequest.getCookies();
+ if (parentCookies != null) {
+ result.setCookies(parentCookies);
+ }
+
+ // request attribute
+ Enumeration parentAttrNames = parentRequest.getAttributeNames();
+ while (parentAttrNames.hasMoreElements()) {
+ String parentAttrName = parentAttrNames.nextElement();
+ result.setAttribute(parentAttrName, parentRequest.getAttribute(parentAttrName));
+ }
+ }
+
+ /**
+ * Set the contextPath to be used.
+ *
The value may be null in which case the first path segment of the
+ * URL is turned into the contextPath. Otherwise it must conform to
+ * {@link HttpServletRequest#getContextPath()} which states it can be
+ * an empty string, or it must start with a "/" and not end with a "/".
+ * @param contextPath a valid contextPath
+ * @throws IllegalArgumentException if the contextPath is not a valid {@link HttpServletRequest#getContextPath()}
+ */
+ public void setContextPath(String contextPath) {
+ MockMvcWebConnection.validateContextPath(contextPath);
+ this.contextPath = contextPath;
+ }
+
+ public void setForwardPostProcessor(RequestPostProcessor forwardPostProcessor) {
+ this.forwardPostProcessor = forwardPostProcessor;
+ }
+
+ private void authType(MockHttpServletRequest request) {
+ String authorization = header("Authorization");
+ if (authorization != null) {
+ String[] authzParts = authorization.split(": ");
+ request.setAuthType(authzParts[0]);
+ }
+ }
+
+ private void content(MockHttpServletRequest result, String charset) {
+ String requestBody = this.webRequest.getRequestBody();
+ if (requestBody == null) {
+ return;
+ }
+ try {
+ result.setContent(requestBody.getBytes(charset));
+ }
+ catch (UnsupportedEncodingException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void contentType(MockHttpServletRequest result) {
+ String contentType = header("Content-Type");
+ result.setContentType(contentType == null ? MediaType.ALL_VALUE.toString() : contentType);
+ }
+
+ private void contextPath(MockHttpServletRequest result, UriComponents uriComponents) {
+ if (this.contextPath == null) {
+ List pathSegments = uriComponents.getPathSegments();
+ if (pathSegments.isEmpty()) {
+ result.setContextPath("");
+ }
+ else {
+ result.setContextPath("/" + pathSegments.get(0));
+ }
+ }
+ else {
+ if (!uriComponents.getPath().startsWith(this.contextPath)) {
+ throw new IllegalArgumentException(uriComponents.getPath() + " should start with contextPath "
+ + this.contextPath);
+ }
+ result.setContextPath(this.contextPath);
+ }
+ }
+
+ private void cookies(MockHttpServletRequest result) {
+ String cookieHeaderValue = header("Cookie");
+ Cookie[] parentCookies = result.getCookies();
+ List cookies = new ArrayList();
+ if (cookieHeaderValue != null) {
+ StringTokenizer tokens = new StringTokenizer(cookieHeaderValue, "=;");
+ while (tokens.hasMoreTokens()) {
+ String cookieName = tokens.nextToken().trim();
+ if (!tokens.hasMoreTokens()) {
+ throw new IllegalArgumentException("Expected value for cookie name '" + cookieName
+ + "'. Full cookie was " + cookieHeaderValue);
+ }
+ String cookieValue = tokens.nextToken().trim();
+ processCookie(result, cookies, new Cookie(cookieName, cookieValue));
+ }
+ }
+
+ Set managedCookies = this.webClient.getCookies(this.webRequest.getUrl());
+ for (com.gargoylesoftware.htmlunit.util.Cookie cookie : managedCookies) {
+ processCookie(result, cookies, new Cookie(cookie.getName(), cookie.getValue()));
+ }
+ if (parentCookies != null) {
+ for (Cookie cookie : parentCookies) {
+ cookies.add(cookie);
+ }
+ }
+ if (!cookies.isEmpty()) {
+ result.setCookies(cookies.toArray(new Cookie[0]));
+ }
+ }
+
+ private void processCookie(MockHttpServletRequest result, List cookies, Cookie cookie) {
+ cookies.add(cookie);
+ if ("JSESSIONID".equals(cookie.getName())) {
+ result.setRequestedSessionId(cookie.getValue());
+ result.setSession(httpSession(result, cookie.getValue()));
+ }
+ }
+
+ private String getCharset() {
+ String charset = this.webRequest.getCharset();
+ if (charset == null) {
+ return "ISO-8859-1";
+ }
+ return charset;
+ }
+
+ private String header(String headerName) {
+ return this.webRequest.getAdditionalHeaders().get(headerName);
+ }
+
+ private void headers(MockHttpServletRequest result) {
+ for (Entry header : this.webRequest.getAdditionalHeaders().entrySet()) {
+ result.addHeader(header.getKey(), header.getValue());
+ }
+ }
+
+ private MockHttpSession httpSession(MockHttpServletRequest request, final String sessionid) {
+ MockHttpSession session;
+ synchronized (this.sessions) {
+ session = this.sessions.get(sessionid);
+ if (session == null) {
+ session = new HtmlUnitMockHttpSession(request, sessionid);
+ session.setNew(true);
+ synchronized (this.sessions) {
+ this.sessions.put(sessionid, session);
+ }
+ addSessionCookie(request, sessionid);
+ }
+ else {
+ session.setNew(false);
+ }
+ }
+ return session;
+ }
+
+ private void addSessionCookie(MockHttpServletRequest request, String sessionid) {
+ getCookieManager().addCookie(createCookie(request, sessionid));
+ }
+
+ private void removeSessionCookie(MockHttpServletRequest request, String sessionid) {
+ getCookieManager().removeCookie(createCookie(request, sessionid));
+ }
+
+ private com.gargoylesoftware.htmlunit.util.Cookie createCookie(MockHttpServletRequest request, String sessionid) {
+ return new com.gargoylesoftware.htmlunit.util.Cookie(request.getServerName(), "JSESSIONID", sessionid,
+ request.getContextPath() + "/", null, request.isSecure(), true);
+ }
+
+ private void locales(MockHttpServletRequest result) {
+ String locale = header("Accept-Language");
+ if (locale == null) {
+ result.addPreferredLocale(Locale.getDefault());
+ }
+ else {
+ String[] locales = locale.split(", ");
+ for (int i = locales.length - 1; i >= 0; i--) {
+ result.addPreferredLocale(parseLocale(locales[i]));
+ }
+ }
+ }
+
+ private void params(MockHttpServletRequest result, UriComponents uriComponents) {
+ for (Entry> values : uriComponents.getQueryParams().entrySet()) {
+ String name = values.getKey();
+ for (String value : values.getValue()) {
+ try {
+ result.addParameter(name, URLDecoder.decode(value, "UTF-8"));
+ }
+ catch (UnsupportedEncodingException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+ for (NameValuePair param : this.webRequest.getRequestParameters()) {
+ result.addParameter(param.getName(), param.getValue());
+ }
+ }
+
+ private Locale parseLocale(String locale) {
+ Matcher matcher = LOCALE_PATTERN.matcher(locale);
+ if (!matcher.matches()) {
+ throw new IllegalArgumentException("Invalid locale " + locale);
+ }
+ String language = matcher.group(1);
+ String country = matcher.group(2);
+ if (country == null) {
+ country = "";
+ }
+ String qualifier = matcher.group(3);
+ if (qualifier == null) {
+ qualifier = "";
+ }
+ return new Locale(language, country, qualifier);
+ }
+
+ private void pathInfo(UriComponents uriComponents, MockHttpServletRequest result) {
+ result.setPathInfo(null);
+ }
+
+ private void servletPath(MockHttpServletRequest result, String requestPath) {
+ String servletPath = requestPath.substring(result.getContextPath().length());
+ if ("".equals(servletPath)) {
+ servletPath = null;
+ }
+ result.setServletPath(servletPath);
+ }
+
+ private void servletPath(UriComponents uriComponents, MockHttpServletRequest result) {
+ if ("".equals(result.getPathInfo())) {
+ result.setPathInfo(null);
+ }
+ servletPath(result, uriComponents.getPath());
+ }
+
+ private void ports(UriComponents uriComponents, MockHttpServletRequest result) {
+ int serverPort = uriComponents.getPort();
+ result.setServerPort(serverPort);
+ if (serverPort == -1) {
+ int portConnection = this.webRequest.getUrl().getDefaultPort();
+ result.setLocalPort(serverPort);
+ result.setRemotePort(portConnection);
+ }
+ else {
+ result.setRemotePort(serverPort);
+ }
+ }
+
+ private UriComponents uriComponents() {
+ URL url = this.webRequest.getUrl();
+ UriComponentsBuilder uriBldr = UriComponentsBuilder.fromUriString(url.toExternalForm());
+ return uriBldr.build();
+ }
+
+ @Override
+ public boolean isMergeEnabled() {
+ return true;
+ }
+
+ @Override
+ public Object merge(Object parent) {
+ if (parent == null) {
+ return this;
+ }
+ if (parent instanceof RequestBuilder) {
+ this.parentBuilder = (RequestBuilder) parent;
+ }
+ if (parent instanceof SmartRequestBuilder) {
+ this.parentPostProcessor = (SmartRequestBuilder) parent;
+ }
+
+ return this;
+ }
+
+
+ /**
+ * An extension to {@link MockHttpServletRequest} that ensures that
+ * when a new {@link HttpSession} is created, it is added to the managed sessions.
+ */
+ private final class HtmlUnitMockHttpServletRequest extends MockHttpServletRequest {
+
+ private HtmlUnitMockHttpServletRequest(ServletContext servletContext, String method, String requestURI) {
+ super(servletContext, method, requestURI);
+ }
+
+ public HttpSession getSession(boolean create) {
+ HttpSession result = super.getSession(false);
+ if (result == null && create) {
+ HtmlUnitMockHttpSession newSession = new HtmlUnitMockHttpSession(this);
+ setSession(newSession);
+ newSession.setNew(true);
+ String sessionid = newSession.getId();
+ synchronized (HtmlUnitRequestBuilder.this.sessions) {
+ HtmlUnitRequestBuilder.this.sessions.put(sessionid, newSession);
+ }
+ addSessionCookie(this, sessionid);
+ result = newSession;
+ }
+ return result;
+ }
+
+ public HttpSession getSession() {
+ return super.getSession();
+ }
+
+ public void setSession(HttpSession session) {
+ super.setSession(session);
+ }
+ }
+
+ /**
+ * An extension to {@link MockHttpSession} that ensures when
+ * {@link #invalidate()} is called that the {@link HttpSession} is
+ * removed from the managed sessions.
+ */
+ private final class HtmlUnitMockHttpSession extends MockHttpSession {
+
+ private final MockHttpServletRequest request;
+
+ private HtmlUnitMockHttpSession(MockHttpServletRequest request) {
+ super(request.getServletContext());
+ this.request = request;
+ }
+
+ private HtmlUnitMockHttpSession(MockHttpServletRequest request, String id) {
+ super(request.getServletContext(), id);
+ this.request = request;
+ }
+
+ public void invalidate() {
+ super.invalidate();
+ synchronized (HtmlUnitRequestBuilder.this.sessions) {
+ HtmlUnitRequestBuilder.this.sessions.remove(getId());
+ }
+ removeSessionCookie(request, getId());
+ }
+ }
+
+ private CookieManager getCookieManager() {
+ return this.webClient.getCookieManager();
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebClientBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebClientBuilder.java
new file mode 100644
index 00000000000..9764cea5c67
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebClientBuilder.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2002-2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.springframework.test.web.servlet.htmlunit;
+
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcConfigurer;
+import org.springframework.util.Assert;
+import org.springframework.web.context.WebApplicationContext;
+
+import com.gargoylesoftware.htmlunit.WebClient;
+
+/**
+ * {@code MockMvcWebClientBuilder} simplifies the creation of a {@link WebClient}
+ * that delegates to a {@link MockMvc} instance.
+ *
+ * @author Rob Winch
+ * @author Sam Brannen
+ * @since 4.2
+ */
+public class MockMvcWebClientBuilder extends MockMvcWebConnectionBuilderSupport {
+
+ protected MockMvcWebClientBuilder(MockMvc mockMvc) {
+ super(mockMvc);
+ }
+
+ protected MockMvcWebClientBuilder(WebApplicationContext context) {
+ super(context);
+ }
+
+ protected MockMvcWebClientBuilder(WebApplicationContext context, MockMvcConfigurer configurer) {
+ super(context, configurer);
+ }
+
+ /**
+ * Create a new instance with the supplied {@link WebApplicationContext}.
+ * @param context the {@code WebApplicationContext} to create a {@link MockMvc}
+ * instance from; never {@code null}
+ * @return the MockMvcWebClientBuilder to customize
+ */
+ public static MockMvcWebClientBuilder webAppContextSetup(WebApplicationContext context) {
+ Assert.notNull(context, "WebApplicationContext must not be null");
+ return new MockMvcWebClientBuilder(context);
+ }
+
+ /**
+ * Create a new instance with the supplied {@link WebApplicationContext}
+ * and {@link MockMvcConfigurer}.
+ * @param context the {@code WebApplicationContext} to create a {@link MockMvc}
+ * instance from; never {@code null}
+ * @param configurer the MockMvcConfigurer to apply; never {@code null}
+ * @return the MockMvcWebClientBuilder to customize
+ */
+ public static MockMvcWebClientBuilder webAppContextSetup(WebApplicationContext context, MockMvcConfigurer configurer) {
+ Assert.notNull(context, "WebApplicationContext must not be null");
+ Assert.notNull(configurer, "MockMvcConfigurer must not be null");
+ return new MockMvcWebClientBuilder(context, configurer);
+ }
+
+ /**
+ * Create a new instance with the supplied {@link MockMvc} instance.
+ * @param mockMvc the {@code MockMvc} instance to use; never {@code null}
+ * @return the MockMvcWebClientBuilder to customize
+ */
+ public static MockMvcWebClientBuilder mockMvcSetup(MockMvc mockMvc) {
+ Assert.notNull(mockMvc, "MockMvc must not be null");
+ return new MockMvcWebClientBuilder(mockMvc);
+ }
+
+ /**
+ * Create a {@link WebClient} that uses the configured {@link MockMvc}
+ * instance for any matching requests and a {@code WebClient} with all
+ * the default settings for any other requests.
+ * @return the {@code WebClient} to use
+ * @see #configureWebClient(WebClient)
+ */
+ public WebClient createWebClient() {
+ return configureWebClient(new WebClient());
+ }
+
+ /**
+ * Configure the supplied {@link WebClient} to use the configured
+ * {@link MockMvc} instance for any matching requests and the supplied
+ * {@code WebClient} for any other requests.
+ * @param webClient the WebClient to delegate to for requests that do not
+ * match; never {@code null}
+ * @return the WebClient to use
+ */
+ public WebClient configureWebClient(WebClient webClient) {
+ Assert.notNull(webClient, "webClient must not be null");
+ webClient.setWebConnection(createConnection(webClient.getWebConnection()));
+ return webClient;
+ }
+
+}
\ No newline at end of file
diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnection.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnection.java
new file mode 100644
index 00000000000..d277f4cef8e
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnection.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2002-2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.springframework.test.web.servlet.htmlunit;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.mock.web.MockHttpSession;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.RequestBuilder;
+import org.springframework.test.web.servlet.ResultActions;
+import org.springframework.test.web.servlet.htmlunit.webdriver.WebConnectionHtmlUnitDriver;
+import org.springframework.util.Assert;
+
+import com.gargoylesoftware.htmlunit.WebClient;
+import com.gargoylesoftware.htmlunit.WebConnection;
+import com.gargoylesoftware.htmlunit.WebRequest;
+import com.gargoylesoftware.htmlunit.WebResponse;
+
+/**
+ * {@code MockMvcWebConnection} enables {@link MockMvc} to transform a
+ * {@link WebRequest} into a {@link WebResponse}.
+ *
+ * WebClient webClient = new WebClient();
+ * MockMvc mockMvc = ...
+ * MockMvcWebConnection webConnection = new MockMvcWebConnection(mockMvc);
+ * mockConnection.setWebClient(webClient);
+ * webClient.setWebConnection(webConnection);
+ *
+ * // Use webClient as normal ...
+ *
+ *
+ * @author Rob Winch
+ * @author Sam Brannen
+ * @since 4.2
+ * @see WebConnectionHtmlUnitDriver
+ */
+public final class MockMvcWebConnection implements WebConnection {
+
+ private final Map sessions = new HashMap();
+
+ private final MockMvc mockMvc;
+
+ private final String contextPath;
+
+ private WebClient webClient;
+
+
+ /**
+ * Create a new instance that assumes the context path of the application
+ * is {@code ""} (i.e., the root context).
+ *
For example, the URL {@code http://localhost/test/this} would use
+ * {@code ""} as the context path.
+ * @param mockMvc the {@code MockMvc} instance to use; never {@code null}
+ */
+ public MockMvcWebConnection(MockMvc mockMvc) {
+ this(mockMvc, "");
+ }
+
+ /**
+ * Create a new instance with the specified context path.
+ *
The path may be {@code null} in which case the first path segment
+ * of the URL is turned into the contextPath. Otherwise it must conform
+ * to {@link javax.servlet.http.HttpServletRequest#getContextPath()}
+ * which states that it can be an empty string and otherwise must start
+ * with a "/" character and not end with a "/" character.
+ * @param mockMvc the {@code MockMvc} instance to use; never {@code null}
+ * @param contextPath the contextPath to use
+ */
+ public MockMvcWebConnection(MockMvc mockMvc, String contextPath) {
+ Assert.notNull(mockMvc, "mockMvc must not be null");
+ validateContextPath(contextPath);
+
+ this.webClient = new WebClient();
+ this.mockMvc = mockMvc;
+ this.contextPath = contextPath;
+ }
+
+ public WebResponse getResponse(WebRequest webRequest) throws IOException {
+ long startTime = System.currentTimeMillis();
+ HtmlUnitRequestBuilder requestBuilder = new HtmlUnitRequestBuilder(this.sessions, this.webClient, webRequest);
+ requestBuilder.setContextPath(this.contextPath);
+
+ MockHttpServletResponse httpServletResponse = getResponse(requestBuilder);
+
+ String forwardedUrl = httpServletResponse.getForwardedUrl();
+ while (forwardedUrl != null) {
+ requestBuilder.setForwardPostProcessor(new ForwardRequestPostProcessor(forwardedUrl));
+ httpServletResponse = getResponse(requestBuilder);
+ forwardedUrl = httpServletResponse.getForwardedUrl();
+ }
+
+ return new MockWebResponseBuilder(startTime, webRequest, httpServletResponse).build();
+ }
+
+ public void setWebClient(WebClient webClient) {
+ Assert.notNull(webClient, "webClient must not be null");
+ this.webClient = webClient;
+ }
+
+ private MockHttpServletResponse getResponse(RequestBuilder requestBuilder) throws IOException {
+ ResultActions resultActions;
+ try {
+ resultActions = this.mockMvc.perform(requestBuilder);
+ }
+ catch (Exception e) {
+ throw (IOException) new IOException(e.getMessage()).initCause(e);
+ }
+
+ return resultActions.andReturn().getResponse();
+ }
+
+ /**
+ * Validate the supplied {@code contextPath}.
+ *
If the value is not {@code null}, it must conform to
+ * {@link javax.servlet.http.HttpServletRequest#getContextPath()} which
+ * states that it can be an empty string and otherwise must start with
+ * a "/" character and not end with a "/" character.
+ * @param contextPath the path to validate
+ */
+ static void validateContextPath(String contextPath) {
+ if (contextPath == null || "".equals(contextPath)) {
+ return;
+ }
+ if (!contextPath.startsWith("/")) {
+ throw new IllegalArgumentException("contextPath '" + contextPath + "' must start with '/'.");
+ }
+ if (contextPath.endsWith("/")) {
+ throw new IllegalArgumentException("contextPath '" + contextPath + "' must not end with '/'.");
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnectionBuilderSupport.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnectionBuilderSupport.java
new file mode 100644
index 00000000000..10bedebaf4f
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnectionBuilderSupport.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2002-2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.springframework.test.web.servlet.htmlunit;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.test.web.servlet.setup.MockMvcConfigurer;
+import org.springframework.util.Assert;
+import org.springframework.web.context.WebApplicationContext;
+
+import com.gargoylesoftware.htmlunit.WebConnection;
+
+/**
+ * Support class that simplifies the creation of a {@link WebConnection} that
+ * uses {@link MockMvc} and optionally delegates to a real {@link WebConnection}
+ * for specific requests.
+ *
+ *
The default is to use {@link MockMvc} for requests to {@code localhost}
+ * and otherwise use a real {@link WebConnection}.
+ *
+ * @author Rob Winch
+ * @author Sam Brannen
+ * @since 4.2
+ */
+public abstract class MockMvcWebConnectionBuilderSupport> {
+
+ private final MockMvc mockMvc;
+
+ private final List mockMvcRequestMatchers = new ArrayList();
+
+ private String contextPath = "";
+
+ private boolean alwaysUseMockMvc;
+
+
+ /**
+ * Create a new instance using the supplied {@link MockMvc} instance.
+ * @param mockMvc the {@code MockMvc} instance to use; never {@code null}
+ */
+ protected MockMvcWebConnectionBuilderSupport(MockMvc mockMvc) {
+ Assert.notNull(mockMvc, "mockMvc must not be null");
+ this.mockMvc = mockMvc;
+ this.mockMvcRequestMatchers.add(new HostRequestMatcher("localhost"));
+ }
+
+ /**
+ * Create a new instance using the supplied {@link WebApplicationContext}.
+ * @param context the {@code WebApplicationContext} to create a {@code MockMvc}
+ * instance from; never {@code null}
+ */
+ protected MockMvcWebConnectionBuilderSupport(WebApplicationContext context) {
+ this(MockMvcBuilders.webAppContextSetup(context).build());
+ }
+
+ /**
+ * Create a new instance using the supplied {@link WebApplicationContext}
+ * and {@link MockMvcConfigurer}.
+ * @param context the {@code WebApplicationContext} to create a {@code MockMvc}
+ * instance from; never {@code null}
+ * @param configurer the MockMvcConfigurer to apply; never {@code null}
+ */
+ protected MockMvcWebConnectionBuilderSupport(WebApplicationContext context, MockMvcConfigurer configurer) {
+ this(MockMvcBuilders.webAppContextSetup(context).apply(configurer).build());
+ }
+
+ /**
+ * Set the context path to use.
+ *
If the supplied value is {@code null} or empty, the first path
+ * segment of the request URL is assumed to be the context path.
+ *
Default is {@code ""}.
+ * @param contextPath the context path to use
+ * @return this builder for further customization
+ */
+ @SuppressWarnings("unchecked")
+ public T contextPath(String contextPath) {
+ this.contextPath = contextPath;
+ return (T) this;
+ }
+
+ /**
+ * Specify that {@link MockMvc} should always be used regardless of
+ * what the request looks like.
+ * @return this builder for further customization
+ */
+ @SuppressWarnings("unchecked")
+ public T alwaysUseMockMvc() {
+ this.alwaysUseMockMvc = true;
+ return (T) this;
+ }
+
+ /**
+ * Add additional {@link WebRequestMatcher} instances that will ensure
+ * that {@link MockMvc} is used to process the request, if such a matcher
+ * matches against the web request.
+ * @param matchers additional {@code WebRequestMatcher} instances
+ * @return this builder for further customization
+ */
+ @SuppressWarnings("unchecked")
+ public T useMockMvc(WebRequestMatcher... matchers) {
+ for (WebRequestMatcher matcher : matchers) {
+ this.mockMvcRequestMatchers.add(matcher);
+ }
+ return (T) this;
+ }
+
+ /**
+ * Add additional {@link WebRequestMatcher} instances that return {@code true}
+ * if a supplied host matches — for example, {@code "example.com"} or
+ * {@code "example.com:8080"}.
+ * @param hosts additional hosts that ensure {@code MockMvc} gets invoked
+ * @return this builder for further customization
+ */
+ @SuppressWarnings("unchecked")
+ public T useMockMvcForHosts(String... hosts) {
+ this.mockMvcRequestMatchers.add(new HostRequestMatcher(hosts));
+ return (T) this;
+ }
+
+ /**
+ * Create a new {@link WebConnection} that will use a {@link MockMvc}
+ * instance if one of the specified {@link WebRequestMatcher} instances
+ * matches.
+ * @param defaultConnection the default WebConnection to use if none of
+ * the specified {@code WebRequestMatcher} instances matches; never {@code null}
+ * @return a new {@code WebConnection} that will use a {@code MockMvc}
+ * instance if one of the specified {@code WebRequestMatcher} matches
+ * @see #alwaysUseMockMvc()
+ * @see #useMockMvc(WebRequestMatcher...)
+ * @see #useMockMvcForHosts(String...)
+ */
+ protected final WebConnection createConnection(WebConnection defaultConnection) {
+ Assert.notNull(defaultConnection, "defaultConnection must not be null");
+ MockMvcWebConnection mockMvcWebConnection = new MockMvcWebConnection(this.mockMvc, this.contextPath);
+
+ if (this.alwaysUseMockMvc) {
+ return mockMvcWebConnection;
+ }
+
+ List delegates = new ArrayList(
+ this.mockMvcRequestMatchers.size());
+ for (WebRequestMatcher matcher : this.mockMvcRequestMatchers) {
+ delegates.add(new DelegatingWebConnection.DelegateWebConnection(matcher, mockMvcWebConnection));
+ }
+
+ return new DelegatingWebConnection(defaultConnection, delegates);
+ }
+
+}
\ No newline at end of file
diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockWebResponseBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockWebResponseBuilder.java
new file mode 100644
index 00000000000..7cf99f13291
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockWebResponseBuilder.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2002-2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.springframework.test.web.servlet.htmlunit;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+import com.gargoylesoftware.htmlunit.WebRequest;
+import com.gargoylesoftware.htmlunit.WebResponse;
+import com.gargoylesoftware.htmlunit.WebResponseData;
+import com.gargoylesoftware.htmlunit.util.NameValuePair;
+
+/**
+ * @author Rob Winch
+ * @author Sam Brannen
+ * @since 4.2
+ */
+final class MockWebResponseBuilder {
+
+ private static final String DEFAULT_STATUS_MESSAGE = "N/A";
+
+ private final long startTime;
+
+ private final WebRequest webRequest;
+
+ private final MockHttpServletResponse response;
+
+
+ public MockWebResponseBuilder(long startTime, WebRequest webRequest, MockHttpServletResponse response) {
+ Assert.notNull(webRequest, "webRequest must not be null");
+ Assert.notNull(response, "response must not be null");
+ this.startTime = startTime;
+ this.webRequest = webRequest;
+ this.response = response;
+ }
+
+ public WebResponse build() throws IOException {
+ WebResponseData webResponseData = webResponseData();
+ long endTime = System.currentTimeMillis();
+ return new WebResponse(webResponseData, webRequest, endTime - startTime);
+ }
+
+ private WebResponseData webResponseData() throws IOException {
+ List responseHeaders = responseHeaders();
+ int statusCode = (this.response.getRedirectedUrl() != null ? HttpStatus.MOVED_PERMANENTLY.value()
+ : this.response.getStatus());
+ String statusMessage = statusMessage(statusCode);
+ return new WebResponseData(this.response.getContentAsByteArray(), statusCode, statusMessage, responseHeaders);
+ }
+
+ private String statusMessage(int statusCode) {
+ String errorMessage = this.response.getErrorMessage();
+ if (StringUtils.hasText(errorMessage)) {
+ return errorMessage;
+ }
+
+ try {
+ return HttpStatus.valueOf(statusCode).getReasonPhrase();
+ }
+ catch (IllegalArgumentException ex) {
+ // ignore
+ }
+
+ return DEFAULT_STATUS_MESSAGE;
+ }
+
+ private List responseHeaders() {
+ Collection headerNames = this.response.getHeaderNames();
+ List responseHeaders = new ArrayList(headerNames.size());
+ for (String headerName : headerNames) {
+ List