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