001/*
002 * Copyright 2002-2017 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.htmlunit;
018
019import java.io.IOException;
020import java.util.Date;
021import java.util.HashMap;
022import java.util.Map;
023
024import com.gargoylesoftware.htmlunit.CookieManager;
025import com.gargoylesoftware.htmlunit.WebClient;
026import com.gargoylesoftware.htmlunit.WebConnection;
027import com.gargoylesoftware.htmlunit.WebRequest;
028import com.gargoylesoftware.htmlunit.WebResponse;
029import com.gargoylesoftware.htmlunit.util.Cookie;
030
031import org.springframework.mock.web.MockHttpServletResponse;
032import org.springframework.mock.web.MockHttpSession;
033import org.springframework.test.web.servlet.MockMvc;
034import org.springframework.test.web.servlet.RequestBuilder;
035import org.springframework.test.web.servlet.ResultActions;
036import org.springframework.util.Assert;
037
038/**
039 * {@code MockMvcWebConnection} enables {@link MockMvc} to transform a
040 * {@link WebRequest} into a {@link WebResponse}.
041 * <p>This is the core integration with <a href="http://htmlunit.sourceforge.net/">HtmlUnit</a>.
042 * <p>Example usage can be seen below.
043 *
044 * <pre class="code">
045 * WebClient webClient = new WebClient();
046 * MockMvc mockMvc = ...
047 * MockMvcWebConnection webConnection = new MockMvcWebConnection(mockMvc, webClient);
048 * webClient.setWebConnection(webConnection);
049 *
050 * // Use webClient as normal ...
051 * </pre>
052 *
053 * @author Rob Winch
054 * @author Sam Brannen
055 * @since 4.2
056 * @see org.springframework.test.web.servlet.htmlunit.webdriver.WebConnectionHtmlUnitDriver
057 */
058public final class MockMvcWebConnection implements WebConnection {
059
060        private final Map<String, MockHttpSession> sessions = new HashMap<String, MockHttpSession>();
061
062        private final MockMvc mockMvc;
063
064        private final String contextPath;
065
066        private WebClient webClient;
067
068
069        /**
070         * Create a new instance that assumes the context path of the application
071         * is {@code ""} (i.e., the root context).
072         * <p>For example, the URL {@code http://localhost/test/this} would use
073         * {@code ""} as the context path.
074         * @param mockMvc the {@code MockMvc} instance to use; never {@code null}
075         * @param webClient the {@link WebClient} to use. never {@code null}
076         */
077        public MockMvcWebConnection(MockMvc mockMvc, WebClient webClient) {
078                this(mockMvc, webClient, "");
079        }
080
081        /**
082         * Create a new instance with the specified context path.
083         * <p>The path may be {@code null} in which case the first path segment
084         * of the URL is turned into the contextPath. Otherwise it must conform
085         * to {@link javax.servlet.http.HttpServletRequest#getContextPath()}
086         * which states that it can be an empty string and otherwise must start
087         * with a "/" character and not end with a "/" character.
088         * @param mockMvc the {@code MockMvc} instance to use (never {@code null})
089         * @param webClient the {@link WebClient} to use (never {@code null})
090         * @param contextPath the contextPath to use
091         */
092        public MockMvcWebConnection(MockMvc mockMvc, WebClient webClient, String contextPath) {
093                Assert.notNull(mockMvc, "MockMvc must not be null");
094                Assert.notNull(webClient, "WebClient must not be null");
095                validateContextPath(contextPath);
096
097                this.webClient = webClient;
098                this.mockMvc = mockMvc;
099                this.contextPath = contextPath;
100        }
101
102        /**
103         * Create a new instance that assumes the context path of the application
104         * is {@code ""} (i.e., the root context).
105         * <p>For example, the URL {@code http://localhost/test/this} would use
106         * {@code ""} as the context path.
107         * @param mockMvc the {@code MockMvc} instance to use; never {@code null}
108         * @deprecated Use {@link #MockMvcWebConnection(MockMvc, WebClient)}
109         */
110        @Deprecated
111        public MockMvcWebConnection(MockMvc mockMvc) {
112                this(mockMvc, "");
113        }
114
115        /**
116         * Create a new instance with the specified context path.
117         * <p>The path may be {@code null} in which case the first path segment
118         * of the URL is turned into the contextPath. Otherwise it must conform
119         * to {@link javax.servlet.http.HttpServletRequest#getContextPath()}
120         * which states that it can be an empty string and otherwise must start
121         * with a "/" character and not end with a "/" character.
122         * @param mockMvc the {@code MockMvc} instance to use; never {@code null}
123         * @param contextPath the contextPath to use
124         * @deprecated use {@link #MockMvcWebConnection(MockMvc, WebClient, String)}
125         */
126        @Deprecated
127        public MockMvcWebConnection(MockMvc mockMvc, String contextPath) {
128                this(mockMvc, new WebClient(), contextPath);
129        }
130
131        /**
132         * Validate the supplied {@code contextPath}.
133         * <p>If the value is not {@code null}, it must conform to
134         * {@link javax.servlet.http.HttpServletRequest#getContextPath()} which
135         * states that it can be an empty string and otherwise must start with
136         * a "/" character and not end with a "/" character.
137         * @param contextPath the path to validate
138         */
139        static void validateContextPath(String contextPath) {
140                if (contextPath == null || "".equals(contextPath)) {
141                        return;
142                }
143                if (!contextPath.startsWith("/")) {
144                        throw new IllegalArgumentException("contextPath '" + contextPath + "' must start with '/'.");
145                }
146                if (contextPath.endsWith("/")) {
147                        throw new IllegalArgumentException("contextPath '" + contextPath + "' must not end with '/'.");
148                }
149        }
150
151
152        public void setWebClient(WebClient webClient) {
153                Assert.notNull(webClient, "WebClient must not be null");
154                this.webClient = webClient;
155        }
156
157
158        public WebResponse getResponse(WebRequest webRequest) throws IOException {
159                long startTime = System.currentTimeMillis();
160                HtmlUnitRequestBuilder requestBuilder = new HtmlUnitRequestBuilder(this.sessions, this.webClient, webRequest);
161                requestBuilder.setContextPath(this.contextPath);
162
163                MockHttpServletResponse httpServletResponse = getResponse(requestBuilder);
164                String forwardedUrl = httpServletResponse.getForwardedUrl();
165                while (forwardedUrl != null) {
166                        requestBuilder.setForwardPostProcessor(new ForwardRequestPostProcessor(forwardedUrl));
167                        httpServletResponse = getResponse(requestBuilder);
168                        forwardedUrl = httpServletResponse.getForwardedUrl();
169                }
170                storeCookies(webRequest, httpServletResponse.getCookies());
171
172                return new MockWebResponseBuilder(startTime, webRequest, httpServletResponse).build();
173        }
174
175        private MockHttpServletResponse getResponse(RequestBuilder requestBuilder) throws IOException {
176                ResultActions resultActions;
177                try {
178                        resultActions = this.mockMvc.perform(requestBuilder);
179                }
180                catch (Exception ex) {
181                        throw new IOException(ex);
182                }
183
184                return resultActions.andReturn().getResponse();
185        }
186
187        private void storeCookies(WebRequest webRequest, javax.servlet.http.Cookie[] cookies) {
188                if (cookies == null) {
189                        return;
190                }
191                Date now = new Date();
192                CookieManager cookieManager = this.webClient.getCookieManager();
193                for (javax.servlet.http.Cookie cookie : cookies) {
194                        if (cookie.getDomain() == null) {
195                                cookie.setDomain(webRequest.getUrl().getHost());
196                        }
197                        Cookie toManage = MockWebResponseBuilder.createCookie(cookie);
198                        Date expires = toManage.getExpires();
199                        if (expires == null || expires.after(now)) {
200                                cookieManager.addCookie(toManage);
201                        }
202                        else {
203                                cookieManager.removeCookie(toManage);
204                        }
205                }
206        }
207
208        @Override
209        public void close() {
210        }
211
212}